diff --git a/.ci/jobs.yml b/.ci/jobs.yml index d6249c28be7ef..fc3e80064fa6f 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -1,6 +1,7 @@ JOB: - kibana-intake - x-pack-intake + - kibana-firefoxSmoke - kibana-ciGroup1 - kibana-ciGroup2 - kibana-ciGroup3 @@ -16,6 +17,7 @@ JOB: # - kibana-visualRegression # make sure all x-pack-ciGroups are listed in test/scripts/jenkins_xpack_ci_group.sh + - x-pack-firefoxSmoke - x-pack-ciGroup1 - x-pack-ciGroup2 - x-pack-ciGroup3 diff --git a/.ci/run.sh b/.ci/run.sh index 21c77a0e345d0..f0a18d929b511 100755 --- a/.ci/run.sh +++ b/.ci/run.sh @@ -20,6 +20,9 @@ kibana-ciGroup*) kibana-visualRegression*) ./test/scripts/jenkins_visual_regression.sh ;; +kibana-firefoxSmoke*) + ./test/scripts/jenkins_firefox_smoke.sh + ;; x-pack-intake) ./test/scripts/jenkins_xpack.sh ;; @@ -30,6 +33,9 @@ x-pack-ciGroup*) x-pack-visualRegression*) ./test/scripts/jenkins_xpack_visual_regression.sh ;; +x-pack-firefoxSmoke*) + ./test/scripts/jenkins_xpack_firefox_smoke.sh + ;; *) echo "JOB '$JOB' is not implemented." exit 1 diff --git a/.gitignore b/.gitignore index efb5c57774633..89406c33208b6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ webpackstats.json !/config/kibana.yml coverage selenium -.babel_register_cache.json +.babelcache.json .webpack.babelcache *.swp *.swo diff --git a/.i18nrc.json b/.i18nrc.json index aa6d953a257c1..3d09f5c3874ef 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -10,6 +10,7 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", + "embeddableApi": "src/legacy/core_plugins/embeddable_api", "kbnVislibVisTypes": "src/legacy/core_plugins/kbn_vislib_vis_types", "markdownVis": "src/legacy/core_plugins/markdown_vis", "metricVis": "src/legacy/core_plugins/metric_vis", @@ -25,8 +26,10 @@ "xpack.apm": "x-pack/plugins/apm", "xpack.beatsManagement": "x-pack/plugins/beats_management", "xpack.canvas": "x-pack/plugins/canvas", + "xpack.code": "x-pack/plugins/code", "xpack.crossClusterReplication": "x-pack/plugins/cross_cluster_replication", "xpack.dashboardMode": "x-pack/plugins/dashboard_mode", + "xpack.fileUpload": "x-pack/plugins/file_upload", "xpack.graph": "x-pack/plugins/graph", "xpack.grokDebugger": "x-pack/plugins/grokdebugger", "xpack.idxMgmt": "x-pack/plugins/index_management", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecb08051cf0a3..630dfedbfe9cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -173,9 +173,9 @@ yarn kbn bootstrap (You can also run `yarn kbn` to see the other available commands. For more info about this tool, see https://github.com/elastic/kibana/tree/master/packages/kbn-pm.) -### Running Elasticsearch +### Running Elasticsearch Locally -There are a few options when it comes to running Elasticsearch: +There are a few options when it comes to running Elasticsearch locally: #### Nightly snapshot (recommended) @@ -213,6 +213,26 @@ node scripts/makelogs --auth : > The default username and password combination are `elastic:changeme` > Make sure to execute `node scripts/makelogs` *after* elasticsearch is up and running! +### Running Elasticsearch Remotely + +You can save some system resources, and the effort of generating sample data, if you have a remote Elasticsearch cluster to connect to. (**Elasticians: you do! Check with your team about where to find credentials**) + +You'll need to [create a `kibana.dev.yml`](#customizing-configkibanadevyml) and add the following to it: + +``` +elasticsearch.hosts: + - {{ url }} +elasticsearch.username: {{ username }} +elasticsearch.password: {{ password }} +elasticsearch.ssl.verificationMode: none +``` + +If many other users will be interacting with your remote cluster, you'll want to add the following to avoid causing conflicts: + +``` +kibana.index: '.{YourGitHubHandle}-kibana' +xpack.task_manager.index: '.{YourGitHubHandle}-task-manager-kibana' +``` ### Running Kibana diff --git a/bin/kibana b/bin/kibana index 39c61808bbc07..222e7247b8492 100755 --- a/bin/kibana +++ b/bin/kibana @@ -21,4 +21,4 @@ if [ ! -x "$NODE" ]; then exit 1 fi -NODE_ENV=production exec "${NODE}" --no-warnings --max-http-header-size=65536 $NODE_OPTIONS "${DIR}/src/cli" ${@} +NODE_ENV=production BROWSERSLIST_IGNORE_OLD_DATA=true exec "${NODE}" --no-warnings --max-http-header-size=65536 $NODE_OPTIONS "${DIR}/src/cli" ${@} diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index f61459c43910c..7f1527fdc2a41 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -22,4 +22,5 @@ export interface CoreStart | [i18n](./kibana-plugin-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-public.i18nstart.md) | | [notifications](./kibana-plugin-public.corestart.notifications.md) | NotificationsStart | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | [overlays](./kibana-plugin-public.corestart.overlays.md) | OverlayStart | [OverlayStart](./kibana-plugin-public.overlaystart.md) | +| [uiSettings](./kibana-plugin-public.corestart.uisettings.md) | UiSettingsStart | [UiSettingsStart](./kibana-plugin-public.uisettingsstart.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.uisettings.md b/docs/development/core/public/kibana-plugin-public.corestart.uisettings.md new file mode 100644 index 0000000000000..7ad3c800a7a1c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.corestart.uisettings.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreStart](./kibana-plugin-public.corestart.md) > [uiSettings](./kibana-plugin-public.corestart.uisettings.md) + +## CoreStart.uiSettings property + +[UiSettingsStart](./kibana-plugin-public.uisettingsstart.md) + +Signature: + +```typescript +uiSettings: UiSettingsStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 6f569f2b3d93c..b3f12e944daa5 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -57,4 +57,5 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | | [ToastInput](./kibana-plugin-public.toastinput.md) | | | [UiSettingsSetup](./kibana-plugin-public.uisettingssetup.md) | | +| [UiSettingsStart](./kibana-plugin-public.uisettingsstart.md) | | diff --git a/docs/development/core/public/kibana-plugin-public.uisettingsstart.md b/docs/development/core/public/kibana-plugin-public.uisettingsstart.md new file mode 100644 index 0000000000000..00a6a9f75da04 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.uisettingsstart.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UiSettingsStart](./kibana-plugin-public.uisettingsstart.md) + +## UiSettingsStart type + + +Signature: + +```typescript +export declare type UiSettingsStart = UiSettingsClient; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md index 88d199fc1b536..1437d5083df2d 100644 --- a/docs/development/core/server/kibana-plugin-server.authenticationhandler.md +++ b/docs/development/core/server/kibana-plugin-server.authenticationhandler.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type AuthenticationHandler = (request: Readonly, t: AuthToolkit) => AuthResult | Promise; +export declare type AuthenticationHandler = (request: KibanaRequest, t: AuthToolkit) => AuthResult | Promise; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authheaders.md b/docs/development/core/server/kibana-plugin-server.authheaders.md new file mode 100644 index 0000000000000..96939cb8bbcbf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthHeaders](./kibana-plugin-server.authheaders.md) + +## AuthHeaders type + +Auth Headers map + +Signature: + +```typescript +export declare type AuthHeaders = Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultdata.headers.md b/docs/development/core/server/kibana-plugin-server.authresultdata.headers.md new file mode 100644 index 0000000000000..4287978c3ac34 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authresultdata.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultData](./kibana-plugin-server.authresultdata.md) > [headers](./kibana-plugin-server.authresultdata.headers.md) + +## AuthResultData.headers property + +Auth specific headers to authenticate a user against Elasticsearch. + +Signature: + +```typescript +headers: AuthHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authresultdata.md b/docs/development/core/server/kibana-plugin-server.authresultdata.md new file mode 100644 index 0000000000000..7ba5771b80d67 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authresultdata.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultData](./kibana-plugin-server.authresultdata.md) + +## AuthResultData interface + +Result of an incoming request authentication. + +Signature: + +```typescript +export interface AuthResultData +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.authresultdata.headers.md) | AuthHeaders | Auth specific headers to authenticate a user against Elasticsearch. | +| [state](./kibana-plugin-server.authresultdata.state.md) | Record<string, unknown> | Data to associate with an incoming request. Any downstream plugin may get access to the data. | + diff --git a/docs/development/core/server/kibana-plugin-server.authresultdata.state.md b/docs/development/core/server/kibana-plugin-server.authresultdata.state.md new file mode 100644 index 0000000000000..3fb8f8e48bded --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.authresultdata.state.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [AuthResultData](./kibana-plugin-server.authresultdata.md) > [state](./kibana-plugin-server.authresultdata.state.md) + +## AuthResultData.state property + +Data to associate with an incoming request. Any downstream plugin may get access to the data. + +Signature: + +```typescript +state: Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md index e28950653b60d..e8e245ac01597 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md @@ -9,5 +9,5 @@ Authentication is successful with given credentials, allow request to pass throu Signature: ```typescript -authenticated: (state?: object) => AuthResult; +authenticated: (data?: Partial) => AuthResult; ``` diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md index f32f7076f0119..2fe4312153a6a 100644 --- a/docs/development/core/server/kibana-plugin-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md @@ -16,7 +16,7 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | -| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (state?: object) => AuthResult | Authentication is successful with given credentials, allow request to pass through | +| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (data?: Partial<AuthResultData>) => AuthResult | Authentication is successful with given credentials, allow request to pass through | | [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | (url: string) => AuthResult | Authentication requires to interrupt request handling and redirect to a configured url | | [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => AuthResult | Authentication is unsuccessful, fail the request with specified error. | diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.(constructor).md b/docs/development/core/server/kibana-plugin-server.clusterclient.(constructor).md index 82046df278a68..7a920c0515646 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.(constructor).md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.(constructor).md @@ -9,7 +9,7 @@ Constructs a new instance of the `ClusterClient` class Signature: ```typescript -constructor(config: ElasticsearchClientConfig, log: Logger); +constructor(config: ElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); ``` ## Parameters @@ -18,4 +18,5 @@ constructor(config: ElasticsearchClientConfig, log: Logger); | --- | --- | --- | | config | ElasticsearchClientConfig | | | log | Logger | | +| getAuthHeaders | GetAuthHeaders | | diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md index d0f7a4c93c69d..d649eab42f086 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.asscoped.md @@ -9,16 +9,14 @@ Creates an instance of `ScopedClusterClient` based on the configuration the curr Signature: ```typescript -asScoped(req?: { - headers?: Headers; - }): ScopedClusterClient; +asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest): ScopedClusterClient; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| req | {
headers?: Headers;
} | Request the ScopedClusterClient instance will be scoped to. | +| request | KibanaRequest | LegacyRequest | FakeRequest | Request the ScopedClusterClient instance will be scoped to. Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.clusterclient.md b/docs/development/core/server/kibana-plugin-server.clusterclient.md index 89b30379e38b8..89ed51762198c 100644 --- a/docs/development/core/server/kibana-plugin-server.clusterclient.md +++ b/docs/development/core/server/kibana-plugin-server.clusterclient.md @@ -16,7 +16,7 @@ export declare class ClusterClient | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(config, log)](./kibana-plugin-server.clusterclient.(constructor).md) | | Constructs a new instance of the ClusterClient class | +| [(constructor)(config, log, getAuthHeaders)](./kibana-plugin-server.clusterclient.(constructor).md) | | Constructs a new instance of the ClusterClient class | ## Properties @@ -28,6 +28,6 @@ export declare class ClusterClient | Method | Modifiers | Description | | --- | --- | --- | -| [asScoped(req)](./kibana-plugin-server.clusterclient.asscoped.md) | | Creates an instance of ScopedClusterClient based on the configuration the current cluster client that exposes additional callAsCurrentUser method scoped to the provided req. Consumers shouldn't worry about closing scoped client instances, these will be automatically closed as soon as the original cluster client isn't needed anymore and closed. | +| [asScoped(request)](./kibana-plugin-server.clusterclient.asscoped.md) | | Creates an instance of ScopedClusterClient based on the configuration the current cluster client that exposes additional callAsCurrentUser method scoped to the provided req. Consumers shouldn't worry about closing scoped client instances, these will be automatically closed as soon as the original cluster client isn't needed anymore and closed. | | [close()](./kibana-plugin-server.clusterclient.close.md) | | Closes the cluster client. After that client cannot be used and one should create a new client instance to be able to interact with Elasticsearch API. | diff --git a/docs/development/core/server/kibana-plugin-server.fakerequest.headers.md b/docs/development/core/server/kibana-plugin-server.fakerequest.headers.md new file mode 100644 index 0000000000000..bd3b6e804d7c0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.fakerequest.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [FakeRequest](./kibana-plugin-server.fakerequest.md) > [headers](./kibana-plugin-server.fakerequest.headers.md) + +## FakeRequest.headers property + +Headers used for authentication against Elasticsearch + +Signature: + +```typescript +headers: Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.fakerequest.md b/docs/development/core/server/kibana-plugin-server.fakerequest.md new file mode 100644 index 0000000000000..c95bb9dabae07 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.fakerequest.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [FakeRequest](./kibana-plugin-server.fakerequest.md) + +## FakeRequest interface + +Fake request object created manually by Kibana plugins. + +Signature: + +```typescript +export interface FakeRequest +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.fakerequest.headers.md) | Record<string, string> | Headers used for authentication against Elasticsearch | + diff --git a/docs/development/core/server/kibana-plugin-server.getauthheaders.md b/docs/development/core/server/kibana-plugin-server.getauthheaders.md new file mode 100644 index 0000000000000..ee7572615fe1a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.getauthheaders.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) + +## GetAuthHeaders type + +Get headers to authenticate a user against Elasticsearch. + +Signature: + +```typescript +export declare type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md b/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md index b86fd76180a24..2818d6ead2c23 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicestart.islistening.md @@ -4,10 +4,10 @@ ## HttpServiceStart.isListening property -Indicates if http server is listening on a port +Indicates if http server is listening on a given port Signature: ```typescript -isListening: () => boolean; +isListening: (port: number) => boolean; ``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicestart.md b/docs/development/core/server/kibana-plugin-server.httpservicestart.md index dbcbbe787a17a..2c9c4c8408f6b 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicestart.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicestart.md @@ -15,5 +15,5 @@ export interface HttpServiceStart | Property | Type | Description | | --- | --- | --- | -| [isListening](./kibana-plugin-server.httpservicestart.islistening.md) | () => boolean | Indicates if http server is listening on a port | +| [isListening](./kibana-plugin-server.httpservicestart.islistening.md) | (port: number) => boolean | Indicates if http server is listening on a given port | diff --git a/docs/development/core/server/kibana-plugin-server.internalcorestart.http.md b/docs/development/core/server/kibana-plugin-server.internalcorestart.http.md deleted file mode 100644 index 0e2a49ae17968..0000000000000 --- a/docs/development/core/server/kibana-plugin-server.internalcorestart.http.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) > [http](./kibana-plugin-server.internalcorestart.http.md) - -## InternalCoreStart.http property - -Signature: - -```typescript -http: HttpServiceStart; -``` diff --git a/docs/development/core/server/kibana-plugin-server.internalcorestart.md b/docs/development/core/server/kibana-plugin-server.internalcorestart.md index 5f6c6617e641c..3974ae0582891 100644 --- a/docs/development/core/server/kibana-plugin-server.internalcorestart.md +++ b/docs/development/core/server/kibana-plugin-server.internalcorestart.md @@ -15,6 +15,5 @@ export interface InternalCoreStart | Property | Type | Description | | --- | --- | --- | -| [http](./kibana-plugin-server.internalcorestart.http.md) | HttpServiceStart | | | [plugins](./kibana-plugin-server.internalcorestart.plugins.md) | PluginsServiceStart | | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.(constructor).md b/docs/development/core/server/kibana-plugin-server.kibanarequest.(constructor).md index f29493c1e5036..216859f4351c9 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.(constructor).md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.(constructor).md @@ -9,7 +9,7 @@ Constructs a new instance of the `KibanaRequest` class Signature: ```typescript -constructor(request: Request, params: Params, query: Query, body: Body); +constructor(request: Request, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean); ``` ## Parameters @@ -20,4 +20,5 @@ constructor(request: Request, params: Params, query: Query, body: Body); | params | Params | | | query | Query | | | body | Body | | +| withoutSecretHeaders | boolean | | diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md index 3c95f91514822..e2e350d33c28f 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.headers.md @@ -4,6 +4,8 @@ ## KibanaRequest.headers property +This property will be removed in future version of this class, please use the `getFilteredHeaders` method instead + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md index a09632febe531..ae658c8ae603a 100644 --- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md @@ -16,14 +16,14 @@ export declare class KibanaRequestKibanaRequest class | +| [(constructor)(request, params, query, body, withoutSecretHeaders)](./kibana-plugin-server.kibanarequest.(constructor).md) | | Constructs a new instance of the KibanaRequest class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [body](./kibana-plugin-server.kibanarequest.body.md) | | Body | | -| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | | +| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers | This property will be removed in future version of this class, please use the getFilteredHeaders method instead | | [params](./kibana-plugin-server.kibanarequest.params.md) | | Params | | | [query](./kibana-plugin-server.kibanarequest.query.md) | | Query | | | [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute> | | @@ -35,3 +35,7 @@ export declare class KibanaRequest + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [LegacyRequest](./kibana-plugin-server.legacyrequest.md) + +## LegacyRequest type + +Support Legacy platform request for the period of migration. + +Signature: + +```typescript +export declare type LegacyRequest = Request; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 7b7d3a9f0662e..3c0699485d98f 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -17,18 +17,21 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [Router](./kibana-plugin-server.router.md) | | +| [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API | ## Interfaces | Interface | Description | | --- | --- | +| [AuthResultData](./kibana-plugin-server.authresultdata.md) | Result of an incoming request authentication. | | [AuthToolkit](./kibana-plugin-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | | [CallAPIOptions](./kibana-plugin-server.callapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | | [DiscoveredPlugin](./kibana-plugin-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | | +| [FakeRequest](./kibana-plugin-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | @@ -43,6 +46,21 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Route specific configuration. | +| [SavedObject](./kibana-plugin-server.savedobject.md) | | +| [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | | +| [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. | +| [SavedObjectsBaseOptions](./kibana-plugin-server.savedobjectsbaseoptions.md) | | +| [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) | | +| [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) | | +| [SavedObjectsBulkResponse](./kibana-plugin-server.savedobjectsbulkresponse.md) | | +| [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | | +| [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | | +| [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) | | +| [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) | | +| [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | A dictionary of saved object type -> version used to determine what migrations need to be applied to a saved object. | +| [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) | | +| [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | | +| [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | | | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | @@ -52,12 +70,17 @@ The plugin integrates with the core system via lifecycle events: `setup` | --- | --- | | [APICaller](./kibana-plugin-server.apicaller.md) | | | [AuthenticationHandler](./kibana-plugin-server.authenticationhandler.md) | | +| [AuthHeaders](./kibana-plugin-server.authheaders.md) | Auth Headers map | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | +| [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [Headers](./kibana-plugin-server.headers.md) | | +| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | Support Legacy platform request for the period of migration. | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [RecursiveReadonly](./kibana-plugin-server.recursivereadonly.md) | | | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | +| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | \#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | +| [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md b/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md new file mode 100644 index 0000000000000..b58408fe5f6b6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [attributes](./kibana-plugin-server.savedobject.attributes.md) + +## SavedObject.attributes property + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.error.md b/docs/development/core/server/kibana-plugin-server.savedobject.error.md new file mode 100644 index 0000000000000..910f1fa32d5df --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.error.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [error](./kibana-plugin-server.savedobject.error.md) + +## SavedObject.error property + +Signature: + +```typescript +error?: { + message: string; + statusCode: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.id.md b/docs/development/core/server/kibana-plugin-server.savedobject.id.md new file mode 100644 index 0000000000000..f0ee1e9770f85 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [id](./kibana-plugin-server.savedobject.id.md) + +## SavedObject.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.md b/docs/development/core/server/kibana-plugin-server.savedobject.md new file mode 100644 index 0000000000000..3b2417c56d5e5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) + +## SavedObject interface + + +Signature: + +```typescript +export interface SavedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [attributes](./kibana-plugin-server.savedobject.attributes.md) | T | | +| [error](./kibana-plugin-server.savedobject.error.md) | {
message: string;
statusCode: number;
} | | +| [id](./kibana-plugin-server.savedobject.id.md) | string | | +| [migrationVersion](./kibana-plugin-server.savedobject.migrationversion.md) | SavedObjectsMigrationVersion | | +| [references](./kibana-plugin-server.savedobject.references.md) | SavedObjectReference[] | | +| [type](./kibana-plugin-server.savedobject.type.md) | string | | +| [updated\_at](./kibana-plugin-server.savedobject.updated_at.md) | string | | +| [version](./kibana-plugin-server.savedobject.version.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md new file mode 100644 index 0000000000000..f9150a96b22cb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.migrationversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [migrationVersion](./kibana-plugin-server.savedobject.migrationversion.md) + +## SavedObject.migrationVersion property + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.references.md b/docs/development/core/server/kibana-plugin-server.savedobject.references.md new file mode 100644 index 0000000000000..08476527a446d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [references](./kibana-plugin-server.savedobject.references.md) + +## SavedObject.references property + +Signature: + +```typescript +references: SavedObjectReference[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.type.md b/docs/development/core/server/kibana-plugin-server.savedobject.type.md new file mode 100644 index 0000000000000..172de52d305f7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [type](./kibana-plugin-server.savedobject.type.md) + +## SavedObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md b/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md new file mode 100644 index 0000000000000..0de1b1a0e816f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.updated_at.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [updated\_at](./kibana-plugin-server.savedobject.updated_at.md) + +## SavedObject.updated\_at property + +Signature: + +```typescript +updated_at?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobject.version.md b/docs/development/core/server/kibana-plugin-server.savedobject.version.md new file mode 100644 index 0000000000000..25fbd536fcc38 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobject.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObject](./kibana-plugin-server.savedobject.md) > [version](./kibana-plugin-server.savedobject.version.md) + +## SavedObject.version property + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md b/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md new file mode 100644 index 0000000000000..b629d8fad0c7b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectattributes.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) + +## SavedObjectAttributes interface + + +Signature: + +```typescript +export interface SavedObjectAttributes +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectreference.id.md b/docs/development/core/server/kibana-plugin-server.savedobjectreference.id.md new file mode 100644 index 0000000000000..f6e11c0228743 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectreference.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) > [id](./kibana-plugin-server.savedobjectreference.id.md) + +## SavedObjectReference.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectreference.md b/docs/development/core/server/kibana-plugin-server.savedobjectreference.md new file mode 100644 index 0000000000000..75cf59ea3b925 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectreference.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) + +## SavedObjectReference interface + +A reference to another saved object. + +Signature: + +```typescript +export interface SavedObjectReference +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-server.savedobjectreference.id.md) | string | | +| [name](./kibana-plugin-server.savedobjectreference.name.md) | string | | +| [type](./kibana-plugin-server.savedobjectreference.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectreference.name.md b/docs/development/core/server/kibana-plugin-server.savedobjectreference.name.md new file mode 100644 index 0000000000000..8a88128c3fbc1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectreference.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) > [name](./kibana-plugin-server.savedobjectreference.name.md) + +## SavedObjectReference.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectreference.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectreference.type.md new file mode 100644 index 0000000000000..5347256dfa2dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectreference.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) > [type](./kibana-plugin-server.savedobjectreference.type.md) + +## SavedObjectReference.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbaseoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbaseoptions.md new file mode 100644 index 0000000000000..6eace924490cc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbaseoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBaseOptions](./kibana-plugin-server.savedobjectsbaseoptions.md) + +## SavedObjectsBaseOptions interface + + +Signature: + +```typescript +export interface SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [namespace](./kibana-plugin-server.savedobjectsbaseoptions.namespace.md) | string | Specify the namespace for this operation | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbaseoptions.namespace.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbaseoptions.namespace.md new file mode 100644 index 0000000000000..6e921dc8ab60e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbaseoptions.namespace.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBaseOptions](./kibana-plugin-server.savedobjectsbaseoptions.md) > [namespace](./kibana-plugin-server.savedobjectsbaseoptions.namespace.md) + +## SavedObjectsBaseOptions.namespace property + +Specify the namespace for this operation + +Signature: + +```typescript +namespace?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.attributes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.attributes.md new file mode 100644 index 0000000000000..cfa19c5fb3fd8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) > [attributes](./kibana-plugin-server.savedobjectsbulkcreateobject.attributes.md) + +## SavedObjectsBulkCreateObject.attributes property + +Signature: + +```typescript +attributes: T; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.id.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.id.md new file mode 100644 index 0000000000000..6b8b65339ffa3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) > [id](./kibana-plugin-server.savedobjectsbulkcreateobject.id.md) + +## SavedObjectsBulkCreateObject.id property + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md new file mode 100644 index 0000000000000..056de4b634b50 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) + +## SavedObjectsBulkCreateObject interface + + +Signature: + +```typescript +export interface SavedObjectsBulkCreateObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [attributes](./kibana-plugin-server.savedobjectsbulkcreateobject.attributes.md) | T | | +| [id](./kibana-plugin-server.savedobjectsbulkcreateobject.id.md) | string | | +| [migrationVersion](./kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | | +| [references](./kibana-plugin-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | +| [type](./kibana-plugin-server.savedobjectsbulkcreateobject.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md new file mode 100644 index 0000000000000..9b33ab9a1b077 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) > [migrationVersion](./kibana-plugin-server.savedobjectsbulkcreateobject.migrationversion.md) + +## SavedObjectsBulkCreateObject.migrationVersion property + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.references.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.references.md new file mode 100644 index 0000000000000..0a96787de03a9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) > [references](./kibana-plugin-server.savedobjectsbulkcreateobject.references.md) + +## SavedObjectsBulkCreateObject.references property + +Signature: + +```typescript +references?: SavedObjectReference[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.type.md new file mode 100644 index 0000000000000..8db44a46d7f3f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkcreateobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) > [type](./kibana-plugin-server.savedobjectsbulkcreateobject.type.md) + +## SavedObjectsBulkCreateObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.fields.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.fields.md new file mode 100644 index 0000000000000..d67df82b123e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.fields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) > [fields](./kibana-plugin-server.savedobjectsbulkgetobject.fields.md) + +## SavedObjectsBulkGetObject.fields property + +SavedObject fields to include in the response + +Signature: + +```typescript +fields?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.id.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.id.md new file mode 100644 index 0000000000000..3476d3276181c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) > [id](./kibana-plugin-server.savedobjectsbulkgetobject.id.md) + +## SavedObjectsBulkGetObject.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.md new file mode 100644 index 0000000000000..ae89f30b9f754 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) + +## SavedObjectsBulkGetObject interface + + +Signature: + +```typescript +export interface SavedObjectsBulkGetObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [fields](./kibana-plugin-server.savedobjectsbulkgetobject.fields.md) | string[] | SavedObject fields to include in the response | +| [id](./kibana-plugin-server.savedobjectsbulkgetobject.id.md) | string | | +| [type](./kibana-plugin-server.savedobjectsbulkgetobject.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.type.md new file mode 100644 index 0000000000000..c3fef3704faa7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkgetobject.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkGetObject](./kibana-plugin-server.savedobjectsbulkgetobject.md) > [type](./kibana-plugin-server.savedobjectsbulkgetobject.type.md) + +## SavedObjectsBulkGetObject.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md new file mode 100644 index 0000000000000..7ff4934a2af66 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkResponse](./kibana-plugin-server.savedobjectsbulkresponse.md) + +## SavedObjectsBulkResponse interface + + +Signature: + +```typescript +export interface SavedObjectsBulkResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [saved\_objects](./kibana-plugin-server.savedobjectsbulkresponse.saved_objects.md) | Array<SavedObject<T>> | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.saved_objects.md b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.saved_objects.md new file mode 100644 index 0000000000000..78f0fe36eaedc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsbulkresponse.saved_objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsBulkResponse](./kibana-plugin-server.savedobjectsbulkresponse.md) > [saved\_objects](./kibana-plugin-server.savedobjectsbulkresponse.saved_objects.md) + +## SavedObjectsBulkResponse.saved\_objects property + +Signature: + +```typescript +saved_objects: Array>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md new file mode 100644 index 0000000000000..3603904c2f89c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) + +## SavedObjectsClientContract type + +\#\# SavedObjectsClient errors + +Since the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either: + +1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) + +Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the `isXYZError()` helpers exposed at `SavedObjectsErrorHelpers` should be used to understand and manage error responses from the `SavedObjectsClient`. + +Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for `error.body.error.type` or doing substring checks on `error.body.error.reason`, just use the helpers to understand the meaning of the error: + +\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 } + +if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know } + +// always rethrow the error unless you handle it throw error; \`\`\` + +\#\#\# 404s from missing index + +From the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing. + +At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages. + +From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing. + +\#\#\# 503s from missing index + +Unlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's `action.auto_create_index` setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated. + +See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) + +Signature: + +```typescript +export declare type SavedObjectsClientContract = Pick; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperfactory.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperfactory.md new file mode 100644 index 0000000000000..321aefcba0ffd --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperfactory.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) + +## SavedObjectsClientWrapperFactory type + +Signature: + +```typescript +export declare type SavedObjectsClientWrapperFactory = (options: SavedObjectsClientWrapperOptions) => SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.client.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.client.md new file mode 100644 index 0000000000000..0545901087bb4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.client.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) > [client](./kibana-plugin-server.savedobjectsclientwrapperoptions.client.md) + +## SavedObjectsClientWrapperOptions.client property + +Signature: + +```typescript +client: SavedObjectsClientContract; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.md new file mode 100644 index 0000000000000..1a096fd9e5264 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) + +## SavedObjectsClientWrapperOptions interface + +Signature: + +```typescript +export interface SavedObjectsClientWrapperOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [client](./kibana-plugin-server.savedobjectsclientwrapperoptions.client.md) | SavedObjectsClientContract | | +| [request](./kibana-plugin-server.savedobjectsclientwrapperoptions.request.md) | Request | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.request.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.request.md new file mode 100644 index 0000000000000..0ff75028612d0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientwrapperoptions.request.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) > [request](./kibana-plugin-server.savedobjectsclientwrapperoptions.request.md) + +## SavedObjectsClientWrapperOptions.request property + +Signature: + +```typescript +request: Request; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.id.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.id.md new file mode 100644 index 0000000000000..1c55342bb0430 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) > [id](./kibana-plugin-server.savedobjectscreateoptions.id.md) + +## SavedObjectsCreateOptions.id property + +(not recommended) Specify an id for the document + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md new file mode 100644 index 0000000000000..61d65bfbf7b90 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) + +## SavedObjectsCreateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | +| [migrationVersion](./kibana-plugin-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | | +| [overwrite](./kibana-plugin-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | +| [references](./kibana-plugin-server.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md new file mode 100644 index 0000000000000..fcbec639312e6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.migrationversion.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) > [migrationVersion](./kibana-plugin-server.savedobjectscreateoptions.migrationversion.md) + +## SavedObjectsCreateOptions.migrationVersion property + +Signature: + +```typescript +migrationVersion?: SavedObjectsMigrationVersion; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.overwrite.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.overwrite.md new file mode 100644 index 0000000000000..cb58e87795300 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.overwrite.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) > [overwrite](./kibana-plugin-server.savedobjectscreateoptions.overwrite.md) + +## SavedObjectsCreateOptions.overwrite property + +Overwrite existing documents (defaults to false) + +Signature: + +```typescript +overwrite?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.references.md b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.references.md new file mode 100644 index 0000000000000..bdf88b021c06c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscreateoptions.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) > [references](./kibana-plugin-server.savedobjectscreateoptions.references.md) + +## SavedObjectsCreateOptions.references property + +Signature: + +```typescript +references?: SavedObjectReference[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createbadrequesterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createbadrequesterror.md new file mode 100644 index 0000000000000..03ad9d29c1cc3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createbadrequesterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [createBadRequestError](./kibana-plugin-server.savedobjectserrorhelpers.createbadrequesterror.md) + +## SavedObjectsErrorHelpers.createBadRequestError() method + +Signature: + +```typescript +static createBadRequestError(reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createesautocreateindexerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createesautocreateindexerror.md new file mode 100644 index 0000000000000..62cec4bfa38fc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createesautocreateindexerror.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [createEsAutoCreateIndexError](./kibana-plugin-server.savedobjectserrorhelpers.createesautocreateindexerror.md) + +## SavedObjectsErrorHelpers.createEsAutoCreateIndexError() method + +Signature: + +```typescript +static createEsAutoCreateIndexError(): DecoratedError; +``` +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.creategenericnotfounderror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.creategenericnotfounderror.md new file mode 100644 index 0000000000000..1abe1cf0067ec --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.creategenericnotfounderror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [createGenericNotFoundError](./kibana-plugin-server.savedobjectserrorhelpers.creategenericnotfounderror.md) + +## SavedObjectsErrorHelpers.createGenericNotFoundError() method + +Signature: + +```typescript +static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | null | | +| id | string | null | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createinvalidversionerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createinvalidversionerror.md new file mode 100644 index 0000000000000..fc65c93fde946 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createinvalidversionerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [createInvalidVersionError](./kibana-plugin-server.savedobjectserrorhelpers.createinvalidversionerror.md) + +## SavedObjectsErrorHelpers.createInvalidVersionError() method + +Signature: + +```typescript +static createInvalidVersionError(versionInput?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| versionInput | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createunsupportedtypeerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createunsupportedtypeerror.md new file mode 100644 index 0000000000000..1b22f86df6796 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.createunsupportedtypeerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [createUnsupportedTypeError](./kibana-plugin-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) + +## SavedObjectsErrorHelpers.createUnsupportedTypeError() method + +Signature: + +```typescript +static createUnsupportedTypeError(type: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoratebadrequesterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoratebadrequesterror.md new file mode 100644 index 0000000000000..deccee473eaa4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoratebadrequesterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [decorateBadRequestError](./kibana-plugin-server.savedobjectserrorhelpers.decoratebadrequesterror.md) + +## SavedObjectsErrorHelpers.decorateBadRequestError() method + +Signature: + +```typescript +static decorateBadRequestError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateconflicterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateconflicterror.md new file mode 100644 index 0000000000000..ac999903d3a21 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateconflicterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [decorateConflictError](./kibana-plugin-server.savedobjectserrorhelpers.decorateconflicterror.md) + +## SavedObjectsErrorHelpers.decorateConflictError() method + +Signature: + +```typescript +static decorateConflictError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateesunavailableerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateesunavailableerror.md new file mode 100644 index 0000000000000..54a420913390b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateesunavailableerror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [decorateEsUnavailableError](./kibana-plugin-server.savedobjectserrorhelpers.decorateesunavailableerror.md) + +## SavedObjectsErrorHelpers.decorateEsUnavailableError() method + +Signature: + +```typescript +static decorateEsUnavailableError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateforbiddenerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateforbiddenerror.md new file mode 100644 index 0000000000000..c5130dfb12400 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorateforbiddenerror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [decorateForbiddenError](./kibana-plugin-server.savedobjectserrorhelpers.decorateforbiddenerror.md) + +## SavedObjectsErrorHelpers.decorateForbiddenError() method + +Signature: + +```typescript +static decorateForbiddenError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorategeneralerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorategeneralerror.md new file mode 100644 index 0000000000000..6086df058483f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decorategeneralerror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [decorateGeneralError](./kibana-plugin-server.savedobjectserrorhelpers.decorategeneralerror.md) + +## SavedObjectsErrorHelpers.decorateGeneralError() method + +Signature: + +```typescript +static decorateGeneralError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoratenotauthorizederror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoratenotauthorizederror.md new file mode 100644 index 0000000000000..3977b58c945bc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoratenotauthorizederror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [decorateNotAuthorizedError](./kibana-plugin-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) + +## SavedObjectsErrorHelpers.decorateNotAuthorizedError() method + +Signature: + +```typescript +static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md new file mode 100644 index 0000000000000..58cba64fd3139 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [decorateRequestEntityTooLargeError](./kibana-plugin-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) + +## SavedObjectsErrorHelpers.decorateRequestEntityTooLargeError() method + +Signature: + +```typescript +static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | | +| reason | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isbadrequesterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isbadrequesterror.md new file mode 100644 index 0000000000000..79805e371884d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isbadrequesterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isBadRequestError](./kibana-plugin-server.savedobjectserrorhelpers.isbadrequesterror.md) + +## SavedObjectsErrorHelpers.isBadRequestError() method + +Signature: + +```typescript +static isBadRequestError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isconflicterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isconflicterror.md new file mode 100644 index 0000000000000..99e636bf006ad --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isconflicterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isConflictError](./kibana-plugin-server.savedobjectserrorhelpers.isconflicterror.md) + +## SavedObjectsErrorHelpers.isConflictError() method + +Signature: + +```typescript +static isConflictError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isesautocreateindexerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isesautocreateindexerror.md new file mode 100644 index 0000000000000..37b845d336203 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isesautocreateindexerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isEsAutoCreateIndexError](./kibana-plugin-server.savedobjectserrorhelpers.isesautocreateindexerror.md) + +## SavedObjectsErrorHelpers.isEsAutoCreateIndexError() method + +Signature: + +```typescript +static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isesunavailableerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isesunavailableerror.md new file mode 100644 index 0000000000000..0672c92a0c80f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isesunavailableerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isEsUnavailableError](./kibana-plugin-server.savedobjectserrorhelpers.isesunavailableerror.md) + +## SavedObjectsErrorHelpers.isEsUnavailableError() method + +Signature: + +```typescript +static isEsUnavailableError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isforbiddenerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isforbiddenerror.md new file mode 100644 index 0000000000000..6350de9b6403c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isforbiddenerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isForbiddenError](./kibana-plugin-server.savedobjectserrorhelpers.isforbiddenerror.md) + +## SavedObjectsErrorHelpers.isForbiddenError() method + +Signature: + +```typescript +static isForbiddenError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isinvalidversionerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isinvalidversionerror.md new file mode 100644 index 0000000000000..c91056b92e456 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isinvalidversionerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isInvalidVersionError](./kibana-plugin-server.savedobjectserrorhelpers.isinvalidversionerror.md) + +## SavedObjectsErrorHelpers.isInvalidVersionError() method + +Signature: + +```typescript +static isInvalidVersionError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isnotauthorizederror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isnotauthorizederror.md new file mode 100644 index 0000000000000..6cedc87f52db9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isnotauthorizederror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isNotAuthorizedError](./kibana-plugin-server.savedobjectserrorhelpers.isnotauthorizederror.md) + +## SavedObjectsErrorHelpers.isNotAuthorizedError() method + +Signature: + +```typescript +static isNotAuthorizedError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isnotfounderror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isnotfounderror.md new file mode 100644 index 0000000000000..125730454e4d0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isnotfounderror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isNotFoundError](./kibana-plugin-server.savedobjectserrorhelpers.isnotfounderror.md) + +## SavedObjectsErrorHelpers.isNotFoundError() method + +Signature: + +```typescript +static isNotFoundError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md new file mode 100644 index 0000000000000..63a8862a6d84c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isRequestEntityTooLargeError](./kibana-plugin-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md) + +## SavedObjectsErrorHelpers.isRequestEntityTooLargeError() method + +Signature: + +```typescript +static isRequestEntityTooLargeError(error: Error | DecoratedError): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | DecoratedError | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.issavedobjectsclienterror.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.issavedobjectsclienterror.md new file mode 100644 index 0000000000000..8e22e2df805f8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.issavedobjectsclienterror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) > [isSavedObjectsClientError](./kibana-plugin-server.savedobjectserrorhelpers.issavedobjectsclienterror.md) + +## SavedObjectsErrorHelpers.isSavedObjectsClientError() method + +Signature: + +```typescript +static isSavedObjectsClientError(error: any): error is DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | any | | + +Returns: + +`error is DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.md new file mode 100644 index 0000000000000..ffa4b06028c2a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectserrorhelpers.md @@ -0,0 +1,40 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) + +## SavedObjectsErrorHelpers class + + +Signature: + +```typescript +export declare class SavedObjectsErrorHelpers +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [createBadRequestError(reason)](./kibana-plugin-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | +| [createEsAutoCreateIndexError()](./kibana-plugin-server.savedobjectserrorhelpers.createesautocreateindexerror.md) | static | | +| [createGenericNotFoundError(type, id)](./kibana-plugin-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | +| [createInvalidVersionError(versionInput)](./kibana-plugin-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | +| [createUnsupportedTypeError(type)](./kibana-plugin-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | +| [decorateBadRequestError(error, reason)](./kibana-plugin-server.savedobjectserrorhelpers.decoratebadrequesterror.md) | static | | +| [decorateConflictError(error, reason)](./kibana-plugin-server.savedobjectserrorhelpers.decorateconflicterror.md) | static | | +| [decorateEsUnavailableError(error, reason)](./kibana-plugin-server.savedobjectserrorhelpers.decorateesunavailableerror.md) | static | | +| [decorateForbiddenError(error, reason)](./kibana-plugin-server.savedobjectserrorhelpers.decorateforbiddenerror.md) | static | | +| [decorateGeneralError(error, reason)](./kibana-plugin-server.savedobjectserrorhelpers.decorategeneralerror.md) | static | | +| [decorateNotAuthorizedError(error, reason)](./kibana-plugin-server.savedobjectserrorhelpers.decoratenotauthorizederror.md) | static | | +| [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | static | | +| [isBadRequestError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isbadrequesterror.md) | static | | +| [isConflictError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isconflicterror.md) | static | | +| [isEsAutoCreateIndexError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isesautocreateindexerror.md) | static | | +| [isEsUnavailableError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | +| [isForbiddenError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | +| [isInvalidVersionError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isinvalidversionerror.md) | static | | +| [isNotAuthorizedError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isnotauthorizederror.md) | static | | +| [isNotFoundError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isnotfounderror.md) | static | | +| [isRequestEntityTooLargeError(error)](./kibana-plugin-server.savedobjectserrorhelpers.isrequestentitytoolargeerror.md) | static | | +| [isSavedObjectsClientError(error)](./kibana-plugin-server.savedobjectserrorhelpers.issavedobjectsclienterror.md) | static | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md new file mode 100644 index 0000000000000..c3b7f35e351ff --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) + +## SavedObjectsFindOptions.defaultSearchOperator property + +Signature: + +```typescript +defaultSearchOperator?: 'AND' | 'OR'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md new file mode 100644 index 0000000000000..6d2cac4f14439 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.fields.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) + +## SavedObjectsFindOptions.fields property + +Signature: + +```typescript +fields?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.hasreference.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.hasreference.md new file mode 100644 index 0000000000000..01d20d898c1ef --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.hasreference.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [hasReference](./kibana-plugin-server.savedobjectsfindoptions.hasreference.md) + +## SavedObjectsFindOptions.hasReference property + +Signature: + +```typescript +hasReference?: { + type: string; + id: string; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md new file mode 100644 index 0000000000000..140b447c0002a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) + +## SavedObjectsFindOptions interface + + +Signature: + +```typescript +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | +| [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[] | | +| [hasReference](./kibana-plugin-server.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | +| [page](./kibana-plugin-server.savedobjectsfindoptions.page.md) | number | | +| [perPage](./kibana-plugin-server.savedobjectsfindoptions.perpage.md) | number | | +| [search](./kibana-plugin-server.savedobjectsfindoptions.search.md) | string | | +| [searchFields](./kibana-plugin-server.savedobjectsfindoptions.searchfields.md) | string[] | see Elasticsearch Simple Query String Query field argument for more information | +| [sortField](./kibana-plugin-server.savedobjectsfindoptions.sortfield.md) | string | | +| [sortOrder](./kibana-plugin-server.savedobjectsfindoptions.sortorder.md) | string | | +| [type](./kibana-plugin-server.savedobjectsfindoptions.type.md) | string | string[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.page.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.page.md new file mode 100644 index 0000000000000..ab6faaf6649ee --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.page.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [page](./kibana-plugin-server.savedobjectsfindoptions.page.md) + +## SavedObjectsFindOptions.page property + +Signature: + +```typescript +page?: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.perpage.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.perpage.md new file mode 100644 index 0000000000000..f775aa450b93a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.perpage.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [perPage](./kibana-plugin-server.savedobjectsfindoptions.perpage.md) + +## SavedObjectsFindOptions.perPage property + +Signature: + +```typescript +perPage?: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md new file mode 100644 index 0000000000000..7dca45e58123f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.search.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [search](./kibana-plugin-server.savedobjectsfindoptions.search.md) + +## SavedObjectsFindOptions.search property + +Signature: + +```typescript +search?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md new file mode 100644 index 0000000000000..fdd157299c144 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.searchfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [searchFields](./kibana-plugin-server.savedobjectsfindoptions.searchfields.md) + +## SavedObjectsFindOptions.searchFields property + +see Elasticsearch Simple Query String Query field argument for more information + +Signature: + +```typescript +searchFields?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.sortfield.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.sortfield.md new file mode 100644 index 0000000000000..3ba2916c3b068 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.sortfield.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [sortField](./kibana-plugin-server.savedobjectsfindoptions.sortfield.md) + +## SavedObjectsFindOptions.sortField property + +Signature: + +```typescript +sortField?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.sortorder.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.sortorder.md new file mode 100644 index 0000000000000..bae922313db34 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.sortorder.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [sortOrder](./kibana-plugin-server.savedobjectsfindoptions.sortorder.md) + +## SavedObjectsFindOptions.sortOrder property + +Signature: + +```typescript +sortOrder?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.type.md new file mode 100644 index 0000000000000..f22eca5e1474c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [type](./kibana-plugin-server.savedobjectsfindoptions.type.md) + +## SavedObjectsFindOptions.type property + +Signature: + +```typescript +type?: string | string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md new file mode 100644 index 0000000000000..e4f7d15042985 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) + +## SavedObjectsFindResponse interface + + +Signature: + +```typescript +export interface SavedObjectsFindResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [page](./kibana-plugin-server.savedobjectsfindresponse.page.md) | number | | +| [per\_page](./kibana-plugin-server.savedobjectsfindresponse.per_page.md) | number | | +| [saved\_objects](./kibana-plugin-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObject<T>> | | +| [total](./kibana-plugin-server.savedobjectsfindresponse.total.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.page.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.page.md new file mode 100644 index 0000000000000..82cd16cd7b48a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.page.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) > [page](./kibana-plugin-server.savedobjectsfindresponse.page.md) + +## SavedObjectsFindResponse.page property + +Signature: + +```typescript +page: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.per_page.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.per_page.md new file mode 100644 index 0000000000000..d93b302488382 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.per_page.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) > [per\_page](./kibana-plugin-server.savedobjectsfindresponse.per_page.md) + +## SavedObjectsFindResponse.per\_page property + +Signature: + +```typescript +per_page: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.saved_objects.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.saved_objects.md new file mode 100644 index 0000000000000..9e4247be4e02d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.saved_objects.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) > [saved\_objects](./kibana-plugin-server.savedobjectsfindresponse.saved_objects.md) + +## SavedObjectsFindResponse.saved\_objects property + +Signature: + +```typescript +saved_objects: Array>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.total.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.total.md new file mode 100644 index 0000000000000..12e86e8d3a4e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindresponse.total.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) > [total](./kibana-plugin-server.savedobjectsfindresponse.total.md) + +## SavedObjectsFindResponse.total property + +Signature: + +```typescript +total: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md new file mode 100644 index 0000000000000..434e46041cf7d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationversion.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) + +## SavedObjectsMigrationVersion interface + +A dictionary of saved object type -> version used to determine what migrations need to be applied to a saved object. + +Signature: + +```typescript +export interface SavedObjectsMigrationVersion +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md new file mode 100644 index 0000000000000..6e0d1a827750c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [addScopedSavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md) + +## SavedObjectsService.addScopedSavedObjectsClientWrapperFactory property + +Signature: + +```typescript +addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider['addClientWrapperFactory']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md new file mode 100644 index 0000000000000..13ccad7ed01ae --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [getSavedObjectsRepository](./kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md) + +## SavedObjectsService.getSavedObjectsRepository() method + +Signature: + +```typescript +getSavedObjectsRepository(...rest: any[]): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| rest | any[] | | + +Returns: + +`any` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md new file mode 100644 index 0000000000000..c762de041edf5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [getScopedSavedObjectsClient](./kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md) + +## SavedObjectsService.getScopedSavedObjectsClient property + +Signature: + +```typescript +getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md new file mode 100644 index 0000000000000..ad281002854b3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) + +## SavedObjectsService interface + + +Signature: + +```typescript +export interface SavedObjectsService +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [addScopedSavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md) | ScopedSavedObjectsClientProvider<Request>['addClientWrapperFactory'] | | +| [getScopedSavedObjectsClient](./kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md) | ScopedSavedObjectsClientProvider<Request>['getClient'] | | +| [SavedObjectsClient](./kibana-plugin-server.savedobjectsservice.savedobjectsclient.md) | typeof SavedObjectsClient | | +| [types](./kibana-plugin-server.savedobjectsservice.types.md) | string[] | | + +## Methods + +| Method | Description | +| --- | --- | +| [getSavedObjectsRepository(rest)](./kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md) | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md new file mode 100644 index 0000000000000..4a7722928e85e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsservice.savedobjectsclient.md) + +## SavedObjectsService.SavedObjectsClient property + +Signature: + +```typescript +SavedObjectsClient: typeof SavedObjectsClient; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md new file mode 100644 index 0000000000000..a783ef4270f18 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [types](./kibana-plugin-server.savedobjectsservice.types.md) + +## SavedObjectsService.types property + +Signature: + +```typescript +types: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.md new file mode 100644 index 0000000000000..577fd632be9cb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) + +## SavedObjectsUpdateOptions interface + + +Signature: + +```typescript +export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [references](./kibana-plugin-server.savedobjectsupdateoptions.references.md) | SavedObjectReference[] | | +| [version](./kibana-plugin-server.savedobjectsupdateoptions.version.md) | string | Ensures version matches that of persisted object | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.references.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.references.md new file mode 100644 index 0000000000000..500be57041756 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.references.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) > [references](./kibana-plugin-server.savedobjectsupdateoptions.references.md) + +## SavedObjectsUpdateOptions.references property + +Signature: + +```typescript +references?: SavedObjectReference[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.version.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.version.md new file mode 100644 index 0000000000000..8461181222238 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateoptions.version.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) > [version](./kibana-plugin-server.savedobjectsupdateoptions.version.md) + +## SavedObjectsUpdateOptions.version property + +Ensures version matches that of persisted object + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.attributes.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.attributes.md new file mode 100644 index 0000000000000..7d1edb3bb6594 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.attributes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) > [attributes](./kibana-plugin-server.savedobjectsupdateresponse.attributes.md) + +## SavedObjectsUpdateResponse.attributes property + +Signature: + +```typescript +attributes: Partial; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md new file mode 100644 index 0000000000000..c49f391df986d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsupdateresponse.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) + +## SavedObjectsUpdateResponse interface + + +Signature: + +```typescript +export interface SavedObjectsUpdateResponse extends Omit, 'attributes'> +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [attributes](./kibana-plugin-server.savedobjectsupdateresponse.attributes.md) | Partial<T> | | + diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md index ed107ae50899b..fcc5b90e2dd0c 100644 --- a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md @@ -7,5 +7,5 @@ Signature: ```typescript -asScoped: (request: Readonly | KibanaRequest) => SessionStorage; +asScoped: (request: KibanaRequest) => SessionStorage; ``` diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md index 8f6f58902fde4..eb559005575b1 100644 --- a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md +++ b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md @@ -16,5 +16,5 @@ export interface SessionStorageFactory | Property | Type | Description | | --- | --- | --- | -| [asScoped](./kibana-plugin-server.sessionstoragefactory.asscoped.md) | (request: Readonly<Request> | KibanaRequest) => SessionStorage<T> | | +| [asScoped](./kibana-plugin-server.sessionstoragefactory.asscoped.md) | (request: KibanaRequest) => SessionStorage<T> | | diff --git a/docs/index.asciidoc b/docs/index.asciidoc index a07922b0af113..1f8cfd3451383 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -54,6 +54,8 @@ include::apm/index.asciidoc[] include::uptime/index.asciidoc[] +include::siem/index.asciidoc[] + include::graph/index.asciidoc[] include::dev-tools.asciidoc[] diff --git a/docs/infrastructure/images/metrics-explorer-screen.png b/docs/infrastructure/images/metrics-explorer-screen.png new file mode 100644 index 0000000000000..7ccf8891678af Binary files /dev/null and b/docs/infrastructure/images/metrics-explorer-screen.png differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc index c841fd73c14a9..c48d78d6cf386 100644 --- a/docs/infrastructure/index.asciidoc +++ b/docs/infrastructure/index.asciidoc @@ -83,3 +83,4 @@ configuration namespace `xpack.infra.sources.default`. See include::monitor.asciidoc[] include::infra-ui.asciidoc[] +include::metrics-explorer.asciidoc[] diff --git a/docs/infrastructure/metrics-explorer.asciidoc b/docs/infrastructure/metrics-explorer.asciidoc new file mode 100644 index 0000000000000..82520e99b42bd --- /dev/null +++ b/docs/infrastructure/metrics-explorer.asciidoc @@ -0,0 +1,58 @@ +[role="xpack"] +[[metrics-explorer]] + +The metrics explorer allows you to easily visualize Metricbeat data and group it by arbitary attributes. This empowers you to visualize multiple metrics and can be a jumping off point for further investigations. + +[role="screenshot"] +image::infrastructure/images/metrics-explorer-screen.png[Metrics Explorer in Kibana] + +[float] +[[metrics-explorer-requirements]] +=== Metrics explorer requirements and considerations + +* The Metric explorer assumes you have data collected from {metricbeat-ref}/metricbeat-overview.html[Metricbeat]. +* You will need read permissions on `metricbeat-*` or the metric index specified in the Infrastructure configuration UI. +* Metrics explorer uses the timestamp field set in the Infrastructure configuration UI. By default that is set to `@timestmap`. +* The interval for the X Axis is set to `auto`. The bucket size is determined by the time range. +* **Open in Visualize** requires the user to have access to the Visualize app, otherwise it will not be available. + +[float] +[[metrics-explorer-tutorial]] +=== Metrics explorer tutorial + +In this tutorial we are going to use the Metrics explorer to create system load charts for each host we are monitoring with Metricbeat. +Once we've explored the system load metrics, +we'll show you how to filter down to a specific host and start exploring outbound network traffic for each interface. +Before we get started, if you don't have any Metricbeat data, you'll need to head over to our +{metricbeat-ref}/metricbeat-overview.html[Metricbeat documentation] and learn how to install and start collection. + +1. Navigate to the Infrastructure UI in Kibana and select **Metrics Explorer** +The initial screen should be empty with the metric field selection open. +2. Start typing `system.load.1` and select the field. +Once you've selected the field, you can add additional metrics for `system.load.5` and `system.load.15`. +3. You should now have a chart with 3 different series for each metric. +By default, the metric explorer will take the average of each field. +To the left of the metric dropdown you will see the aggregation dropdown. +You can use this to change the aggregation. +For now, we'll leave it set to `Average`, but take some time to play around with the different aggregations. +4. To the right of the metric input field you will see **graph per** and a dropdown. +Enter `host.name` in this dropdown and select the field. +This input will create a chart for every value it finds in the selected field. +5. By now, your UI should look similar to the screenshot above. +If you only have one host, then it will display the chart across the entire screen. +For multiple hosts, the metric explorer divides the screen into three columns. +Configurations, you've explored your first metric! +6. Let's go for some bonus points. Select the **Actions** dropdown in the upper right hand corner of one of the charts. +Select **Add Filter** to change the KQL expression to filter for that specific host. +From here we can start exploring other metrics specific to this host. +7. Let's delete each of the system load metrics by clicking the little **X** icon next to each of them. +8. Set `system.network.out.bytes` as the metric. +Because `system.network.out.bytes` is a monotonically increasing number, we need to change the aggregation to `Rate`. +While this chart might appear correct, there is one critical problem: hosts have multiple interfaces. +9. To fix our chart, set the group by dropdown to `system.network.name`. +You should now see a chart per network interface. +10. Let's imagine you want to put one of these charts on a dashboard. +Click the **Actions** menu next to one of the interface charts and select **Open In Visualize**. +This will open the same chart in Time Series Visual Builder. From here you can save the chart and add it to a dashboard. + +Who's the Metrics explorer now? You are! diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index 65cdc11975562..684a7247d5215 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -23,3 +23,4 @@ The index must contain at least one field mapped as {ref}/geo-point.html[geo_poi include::terms-join.asciidoc[] include::vector-style.asciidoc[] +include::vector-style-properties.asciidoc[] diff --git a/docs/maps/vector-style-properties.asciidoc b/docs/maps/vector-style-properties.asciidoc new file mode 100644 index 0000000000000..a7c81ec0d5df0 --- /dev/null +++ b/docs/maps/vector-style-properties.asciidoc @@ -0,0 +1,44 @@ +[role="xpack"] +[[maps-vector-style-properties]] +=== Vector style properties + +Point, polygon, and line features support different styling properties. + +[float] +[[point-style-properties]] +==== Point style properties + +*Fill color*:: The fill color of the point features. + +*Border color*:: The border color of the point features. +Only applies when the point feature is symbolized as a circle. + +*Border width*:: The border width of the point features. +Only applies when the point feature is symbolized as a circle. + +*Symbol*:: Specify whether to symbolize the point as a circle or icon. + +*Symbol orientation*:: The symbol orientation rotating the icon clockwise. +Only applies when the point feature is symbolized as an icon. + +*Symbol size*:: The radius of the symbol size, in pixels. + + +[float] +[[polygon-style-properties]] +==== Polygon style properties + +*Fill color*:: The fill color of the polygon features. + +*Border color*:: The border color of the polygon features. + +*Border width*:: The border width of the polygon features. + + +[float] +[[line-style-properties]] +==== Line style properties + +*Border color*:: The color of the line features. + +*Border width*:: The width of the line features. diff --git a/docs/maps/vector-style.asciidoc b/docs/maps/vector-style.asciidoc index 7f501e542b755..daa0eab3703e9 100644 --- a/docs/maps/vector-style.asciidoc +++ b/docs/maps/vector-style.asciidoc @@ -3,25 +3,7 @@ === Vector styling When styling a vector layer, you can customize your data by property, such as size and color. -For each property, you can specify whether to use a constant or dynamic value for the style. - -[float] -[[maps-vector-style-properties]] -==== Style properties - -You can configure the following properties. - -*Fill color*:: The fill color of the vector features. -+ -NOTE: *LineString* and *MultiLineString* geometries do not have fill and do not use the fill color property. -Set border color to style line geometries. - -*Border color*:: The border color of the vector features. - -*Border width*:: The border width of the vector features. - -*Symbol size*:: The symbol size of point features. - +For each property, you can specify whether to use a constant or data driven value for the style. [float] [[maps-vector-style-static]] @@ -41,8 +23,14 @@ image::maps/images/vector_style_static.png[] Use data driven styling to symbolize features from a range of numeric property values. To enable data driven styling, click image:maps/images/gs_link_icon.png[] next to the property. - -NOTE: The image:maps/images/gs_link_icon.png[] button is only available for vector features that contain numeric properties. +This button is only available when vector features contain numeric properties. + +NOTE: The *Fill color* and *Border color* style properties are set to transparent and are not visible +when the property value is undefined for a feature. +The *Border width* and *Symbol size* style properties are set to the minimum size +when the property value is undefined for a feature. +The *Symbol orientation* style property is set to 0 +when the property value is undefined for a feature. The image below shows an example of data driven styling using the <> data set. The *kibana_sample_data_logs* layer uses data driven styling for fill color and symbol size style properties. diff --git a/docs/monitoring/elasticsearch-details.asciidoc b/docs/monitoring/elasticsearch-details.asciidoc index dd48b1663beda..c644407e9a16f 100644 --- a/docs/monitoring/elasticsearch-details.asciidoc +++ b/docs/monitoring/elasticsearch-details.asciidoc @@ -7,8 +7,12 @@ ++++ You can drill down into the status of your {es} cluster in {kib} by clicking -the <>, <>, and -<> links on the *Monitoring* page. +the <>, <>, +<> and <> links on the +*Stack Monitoring* page. + +[role="screenshot"] +image::monitoring/images/monitoring-elasticsearch.jpg["Monitoring clusters"] See also {ref}/es-monitoring.html[Monitoring {es}]. @@ -23,13 +27,15 @@ highlighted in yellow or red. TIP: Conditions that require your attention are listed at the top of the Clusters page. You can also set up watches to alert you when the status of your cluster changes. To learn how, see -{xpack-ref}/watch-cluster-status.html[Watch Your Cluster Health]. +{stack-ov}/watch-cluster-status.html[Watch Your Cluster Health]. The panel at the top shows the current cluster statistics, the charts show the search and indexing performance over time, and the table at the bottom shows -information about any shards that are being recovered. +information about any shards that are being recovered. If you use {filebeat} to +collect log data from this cluster, you can also see its recent logs. -image::monitoring/images/monitoring-overview.png["Elasticsearch Cluster Overview",link="images/monitoring-overview.png"] +[role="screenshot"] +image::monitoring/images/monitoring-overview.jpg["Elasticsearch Cluster Overview"] TIP: Not sure what a chart is showing? Click the info button for a description of the metrics. @@ -43,16 +49,20 @@ From there, you can dive into detailed metrics for particular nodes and indices. To view node metrics, click **Nodes**. The Nodes section shows the status of each node in your cluster. -image::monitoring/images/monitoring-nodes.png["Elasticsearch Nodes",link="images/monitoring-nodes.png"] +[role="screenshot"] +image::monitoring/images/monitoring-nodes.jpg["Elasticsearch Nodes"] [float] [[nodes-page-overview]] ===== Node Overview Click the name of a node to view its node statistics over time. These represent -high-level statistics collected from {es} that provide a good overview of health. +high-level statistics collected from {es} that provide a good overview of +health. If you use {filebeat} to collect log data from this node, you can also +see its recent logs. -image::monitoring/images/monitoring-node.png["Elasticsearch Node Overview",link="images/monitoring-node.png"] +[role="screenshot"] +image::monitoring/images/monitoring-node.jpg["Elasticsearch Node Overview"] [float] [[nodes-page-advanced]] @@ -62,7 +72,8 @@ To view advanced node metrics, click the **Advanced** tab for a node. The *Advanced* tab shows additional metrics, such as memory and garbage collection statistics reported by the selected {es} node. -image::monitoring/images/monitoring-node-advanced.png["Elasticsearch Node Advanced",link="images/monitoring-node-advanced.png"] +[role="screenshot"] +image::monitoring/images/monitoring-node-advanced.jpg["Elasticsearch Node Advanced"] You can use the advanced node view to diagnose issues that generally involve more advanced knowledge of {es}, such as poor garbage collection performance. @@ -75,7 +86,8 @@ more advanced knowledge of {es}, such as poor garbage collection performance. To view index metrics, click **Indices**. The Indices section shows the same overall index and search metrics as the Overview and a table of your indices. -image::monitoring/images/monitoring-indices.png["Elasticsearch Indices",link="images/monitoring-indices.png"] +[role="screenshot"] +image::monitoring/images/monitoring-indices.jpg["Elasticsearch Indices"] [float] [[indices-page-overview]] @@ -84,7 +96,8 @@ image::monitoring/images/monitoring-indices.png["Elasticsearch Indices",link="im From the Indices listing, you can view data for a particular index. To drill down into the data for a particular index, click its name in the Indices table. -image::monitoring/images/monitoring-index.png["Elasticsearch Index Overview",link="images/monitoring-index.png"] +[role="screenshot"] +image::monitoring/images/monitoring-index.jpg["Elasticsearch Index Overview"] [float] [[indices-page-advanced]] @@ -95,7 +108,8 @@ To view advanced index metrics, click the **Advanced** tab for an index. The about the {es} index. If the index has more than one shard, then its shards might live on more than one node. -image::monitoring/images/monitoring-index-advanced.png["Elasticsearch Index Advanced",link="images/monitoring-index-advanced.png"] +[role="screenshot"] +image::monitoring/images/monitoring-index-advanced.jpg["Elasticsearch Index Advanced"] The Advanced index view can be used to diagnose issues that generally involve more advanced knowledge of {es}, such as wasteful index memory usage. @@ -108,26 +122,46 @@ To view {ml} job metrics, click **Jobs**. For each job in your cluster, it shows information such as its status, the number of records processed, the size of the model, the number of forecasts, and the node that runs the job. +[role="screenshot"] image::monitoring/images/monitoring-jobs.png["Machine learning jobs",link="images/monitoring-jobs.png"] [float] [[ccr-overview-page]] ==== CCR -beta[] - To view {ccr} metrics, click **CCR**. For each follower index on the cluster, it shows information such as the leader index, an indication of how much the follower index is lagging behind the leader index, the last fetch time, the number of operations synced, and error messages. If you select a follower index, you can view the same information for each shard. For example: +[role="screenshot"] image::monitoring/images/monitoring-ccr.png["Cross-cluster replication",link="images/monitoring-ccr.png"] If you select a shard, you can see graphs for the fetch and operation delays. You can also see advanced information, which contains the results from the {ref}/ccr-get-follow-stats.html[get follower stats API]. For example: - + +[role="screenshot"] image::monitoring/images/monitoring-ccr-shard.png["Cross-cluster replication shard details",link="images/monitoring-ccr-shard.png"] For more information, see {stack-ov}/xpack-ccr.html[Cross-cluster replication]. + +[float] +[[logs-monitor-page]] +==== Logs + +If you use {filebeat} to collect log data from your cluster, you can see its +recent logs in the *Stack Monitoring* application. The *Clusters* page lists the +number of informational, debug, and warning messages in the server and +deprecation logs. + +If you click *Logs*, you can see the most recent logs for the cluster. For +example: + +[role="screenshot"] +image::monitoring/images/monitoring-elasticsearch-logs.jpg["Recent {es} logs"] + +TIP: By default, up to 10 log entries are shown. You can show up to 50 log +entries by changing the +<>. diff --git a/docs/monitoring/images/monitoring-elasticsearch-logs.jpg b/docs/monitoring/images/monitoring-elasticsearch-logs.jpg new file mode 100644 index 0000000000000..bdcf924652e19 Binary files /dev/null and b/docs/monitoring/images/monitoring-elasticsearch-logs.jpg differ diff --git a/docs/monitoring/images/monitoring-elasticsearch.jpg b/docs/monitoring/images/monitoring-elasticsearch.jpg new file mode 100644 index 0000000000000..5ba8624d24c4b Binary files /dev/null and b/docs/monitoring/images/monitoring-elasticsearch.jpg differ diff --git a/docs/monitoring/images/monitoring-index-advanced.jpg b/docs/monitoring/images/monitoring-index-advanced.jpg new file mode 100644 index 0000000000000..61f8e99f5ada6 Binary files /dev/null and b/docs/monitoring/images/monitoring-index-advanced.jpg differ diff --git a/docs/monitoring/images/monitoring-index-advanced.png b/docs/monitoring/images/monitoring-index-advanced.png deleted file mode 100644 index 833ae3a0d7638..0000000000000 Binary files a/docs/monitoring/images/monitoring-index-advanced.png and /dev/null differ diff --git a/docs/monitoring/images/monitoring-index.jpg b/docs/monitoring/images/monitoring-index.jpg new file mode 100644 index 0000000000000..7a6b6d9cff850 Binary files /dev/null and b/docs/monitoring/images/monitoring-index.jpg differ diff --git a/docs/monitoring/images/monitoring-index.png b/docs/monitoring/images/monitoring-index.png deleted file mode 100644 index d510e2fb7ff25..0000000000000 Binary files a/docs/monitoring/images/monitoring-index.png and /dev/null differ diff --git a/docs/monitoring/images/monitoring-indices.jpg b/docs/monitoring/images/monitoring-indices.jpg new file mode 100644 index 0000000000000..91e1bc05cfd3b Binary files /dev/null and b/docs/monitoring/images/monitoring-indices.jpg differ diff --git a/docs/monitoring/images/monitoring-indices.png b/docs/monitoring/images/monitoring-indices.png deleted file mode 100644 index 41904fa1f5941..0000000000000 Binary files a/docs/monitoring/images/monitoring-indices.png and /dev/null differ diff --git a/docs/monitoring/images/monitoring-jobs.png b/docs/monitoring/images/monitoring-jobs.png index bf454f1edb01d..531f37ecae8ec 100644 Binary files a/docs/monitoring/images/monitoring-jobs.png and b/docs/monitoring/images/monitoring-jobs.png differ diff --git a/docs/monitoring/images/monitoring-node-advanced.jpg b/docs/monitoring/images/monitoring-node-advanced.jpg new file mode 100644 index 0000000000000..9bdb626104016 Binary files /dev/null and b/docs/monitoring/images/monitoring-node-advanced.jpg differ diff --git a/docs/monitoring/images/monitoring-node-advanced.png b/docs/monitoring/images/monitoring-node-advanced.png deleted file mode 100644 index 74aa967ab1ae8..0000000000000 Binary files a/docs/monitoring/images/monitoring-node-advanced.png and /dev/null differ diff --git a/docs/monitoring/images/monitoring-node.jpg b/docs/monitoring/images/monitoring-node.jpg new file mode 100644 index 0000000000000..cdfd8ba6632de Binary files /dev/null and b/docs/monitoring/images/monitoring-node.jpg differ diff --git a/docs/monitoring/images/monitoring-node.png b/docs/monitoring/images/monitoring-node.png deleted file mode 100644 index dd798fb19ea6b..0000000000000 Binary files a/docs/monitoring/images/monitoring-node.png and /dev/null differ diff --git a/docs/monitoring/images/monitoring-nodes.jpg b/docs/monitoring/images/monitoring-nodes.jpg new file mode 100644 index 0000000000000..2e668a0c91174 Binary files /dev/null and b/docs/monitoring/images/monitoring-nodes.jpg differ diff --git a/docs/monitoring/images/monitoring-nodes.png b/docs/monitoring/images/monitoring-nodes.png deleted file mode 100644 index ea0ca24b273df..0000000000000 Binary files a/docs/monitoring/images/monitoring-nodes.png and /dev/null differ diff --git a/docs/monitoring/images/monitoring-overview.jpg b/docs/monitoring/images/monitoring-overview.jpg new file mode 100644 index 0000000000000..2362e1992af3c Binary files /dev/null and b/docs/monitoring/images/monitoring-overview.jpg differ diff --git a/docs/monitoring/images/monitoring-overview.png b/docs/monitoring/images/monitoring-overview.png deleted file mode 100644 index f58b10a2e0488..0000000000000 Binary files a/docs/monitoring/images/monitoring-overview.png and /dev/null differ diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 05d1a4089e2df..74a7e4b61170b 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -91,10 +91,9 @@ However, the defaults work best in most circumstances. For more information about configuring {kib}, see {kibana-ref}/settings.html[Setting Kibana Server Properties]. -`xpack.monitoring.ui.enabled`:: -Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end -continues to run as an agent for sending {kib} stats to the monitoring -cluster. Defaults to `true`. +`xpack.monitoring.elasticsearch.logFetchCount`:: +Specifies the number of log entries to display in the Monitoring UI. Defaults to +`10`. The maximum value is `50`. `xpack.monitoring.max_bucket_size`:: Specifies the number of term buckets to return out of the overall terms list when @@ -109,6 +108,11 @@ represent. Defaults to 10. If you modify the `xpack.monitoring.collection.interval` in `elasticsearch.yml`, use the same value in this setting. +`xpack.monitoring.ui.enabled`:: +Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end +continues to run as an agent for sending {kib} stats to the monitoring +cluster. Defaults to `true`. + [float] [[monitoring-ui-cgroup-settings]] ===== Monitoring UI container settings diff --git a/docs/siem/images/hosts-ui.png b/docs/siem/images/hosts-ui.png new file mode 100644 index 0000000000000..2569df8f419b8 Binary files /dev/null and b/docs/siem/images/hosts-ui.png differ diff --git a/docs/siem/images/network-ui.png b/docs/siem/images/network-ui.png new file mode 100644 index 0000000000000..34cde749cbbb8 Binary files /dev/null and b/docs/siem/images/network-ui.png differ diff --git a/docs/siem/images/overview-ui.png b/docs/siem/images/overview-ui.png new file mode 100644 index 0000000000000..a34b2fea061c9 Binary files /dev/null and b/docs/siem/images/overview-ui.png differ diff --git a/docs/siem/images/timeline-ui.png b/docs/siem/images/timeline-ui.png new file mode 100644 index 0000000000000..9a9c1d9c790e7 Binary files /dev/null and b/docs/siem/images/timeline-ui.png differ diff --git a/docs/siem/index.asciidoc b/docs/siem/index.asciidoc new file mode 100644 index 0000000000000..4606f351d6d6e --- /dev/null +++ b/docs/siem/index.asciidoc @@ -0,0 +1,54 @@ +[role="xpack"] +[[xpack-siem]] += SIEM + +[partintro] +-- +coming[7.2] + +beta[] + +The SIEM app in Kibana provides an interactive workspace for security teams to +triage events and perform initial investigations. It enables analysis of +host-related and network-related security events as part of alert investigations +or interactive threat hunting. + + +[role="screenshot"] +image::siem/images/overview-ui.png[SIEM Overview in Kibana] + + +[float] +== Add data + +Kibana provides step-by-step instructions to help you add data. The +{siem-guide}[SIEM Guide] is a good source for more +detailed information and instructions. + +[float] +=== {Beats} + +https://www.elastic.co/products/beats/auditbeat[{auditbeat}], +https://www.elastic.co/products/beats/filebeat[{filebeat}], +https://www.elastic.co/products/beats/winlogbeat[{winlogbeat}], and +https://www.elastic.co/products/beats/packetbeat[{packetbeat}] +send security events and other data to Elasticsearch. + +The default index patterns for SIEM events are `auditbeat-*`, `winlogbeat-*`, +`filebeat-*`, and `packetbeat-*``. You can change the default index patterns in +*Kibana > Management > Advanced Settings > siem:defaultIndex*. + +[float] +=== Elastic Common Schema (ECS) for normalizing data + +The {ecs-ref}[Elastic Common Schema (ECS)] defines a common set of fields to be +used for storing event data in Elasticsearch. ECS helps users normalize their +event data to better analyze, visualize, and correlate the data represented in +their events. + +SIEM can ingest and normalize events from ECS-compatible data sources. + +-- + + +include::siem-ui.asciidoc[] diff --git a/docs/siem/siem-ui.asciidoc b/docs/siem/siem-ui.asciidoc new file mode 100644 index 0000000000000..7294cfbc414f4 --- /dev/null +++ b/docs/siem/siem-ui.asciidoc @@ -0,0 +1,73 @@ +[role="xpack"] +[[siem-ui]] +== Using the SIEM UI + +The SIEM app is a highly interactive workspace for security analysts. It is +designed to be discoverable, clickable, draggable and droppable, expandable and +collapsible, resizable, moveable, and so forth. You start with an overview. Then +you can use the interactive UI to drill down into areas of interest. + +[float] +[[hosts-ui]] +=== Hosts + +The Hosts view provides key metrics regarding host-related security events, and +data tables and widgets that let you interact with the Timeline Event Viewer. +You can drill down for deeper insights, and drag and drop items of interest from +the Hosts view tables to Timeline for further investigation. + +[role="screenshot"] +image::siem/images/hosts-ui.png[] + + +[float] +[[network-ui]] +=== Network + +The Network view provides key network activity metrics, facilitates +investigation time enrichment, and provides network event tables that enable +interaction with the Timeline. You can drill down for deeper insights, and drag +and drop items of interest from the Network view to Timeline for further +investigation. + +[role="screenshot"] +image::siem/images/network-ui.png[] + +[float] +[[timelines-ui]] +=== Timeline + +Timeline is your workspace for threat hunting and alert investigations. + +[role="screenshot"] +image::siem/images/timeline-ui.png[SIEM Timeline] + +You can drag objects of interest into the Timeline Event Viewer to create +exactly the query filter you need. You can drag items from table widgets within +Hosts and Network pages, or even from within Timeline itself. + +A timeline is responsive and persists as you move through the SIEM app +collecting data. + +See the {siem-guide}[SIEM Guide] for more details on data sources and an +overview of UI elements and capabilities. + +[float] +[[sample-workflow]] +=== Sample workflow + +An analyst notices a suspicious user ID that warrants further investigation, and +clicks a url that links to the SIEM app. + +The analyst uses the tables, widgets, and filtering and search capabilities in +the SIEM app to get to the bottom of the alert. The analyst can drag items of +interest to the timeline for further analysis. + +Within the timeline, the analyst can investigate further--drilling down, +searching, and filtering--and add notes and pin items of interest. + +The analyst can name the timeline, write summary notes, and share it with others +if appropriate. + + + diff --git a/docs/uptime/images/check-history.png b/docs/uptime/images/check-history.png index de5b8da2e0376..6418495eee9ed 100644 Binary files a/docs/uptime/images/check-history.png and b/docs/uptime/images/check-history.png differ diff --git a/docs/uptime/images/crosshair-example.png b/docs/uptime/images/crosshair-example.png index a9384419b0cfe..a4559eac1c3e7 100644 Binary files a/docs/uptime/images/crosshair-example.png and b/docs/uptime/images/crosshair-example.png differ diff --git a/docs/uptime/images/error-list.png b/docs/uptime/images/error-list.png index efdb9d43da34e..99f017f2945a5 100644 Binary files a/docs/uptime/images/error-list.png and b/docs/uptime/images/error-list.png differ diff --git a/docs/uptime/images/filter-bar.png b/docs/uptime/images/filter-bar.png index c64b5c609ab0b..dee735d0f4907 100644 Binary files a/docs/uptime/images/filter-bar.png and b/docs/uptime/images/filter-bar.png differ diff --git a/docs/uptime/images/monitor-charts.png b/docs/uptime/images/monitor-charts.png index 7bc5ecbad4a41..dbfa43f47656e 100644 Binary files a/docs/uptime/images/monitor-charts.png and b/docs/uptime/images/monitor-charts.png differ diff --git a/docs/uptime/images/monitor-list.png b/docs/uptime/images/monitor-list.png index 591d94a5e4965..0c8ad473428bd 100644 Binary files a/docs/uptime/images/monitor-list.png and b/docs/uptime/images/monitor-list.png differ diff --git a/docs/uptime/images/observability_integrations.png b/docs/uptime/images/observability_integrations.png new file mode 100644 index 0000000000000..d5c612c7589ca Binary files /dev/null and b/docs/uptime/images/observability_integrations.png differ diff --git a/docs/uptime/images/snapshot-view.png b/docs/uptime/images/snapshot-view.png index 87a888449af57..020396d0f3e4c 100644 Binary files a/docs/uptime/images/snapshot-view.png and b/docs/uptime/images/snapshot-view.png differ diff --git a/docs/uptime/images/status-bar.png b/docs/uptime/images/status-bar.png index 0287ffd9cb2a3..e0e9b27555900 100644 Binary files a/docs/uptime/images/status-bar.png and b/docs/uptime/images/status-bar.png differ diff --git a/docs/uptime/overview.asciidoc b/docs/uptime/overview.asciidoc index 5a421abb32bbc..5105bffb3a33f 100644 --- a/docs/uptime/overview.asciidoc +++ b/docs/uptime/overview.asciidoc @@ -49,6 +49,18 @@ way to navigate to a more in-depth visualization for interesting hosts or endpoi This table includes information like the most recent status, when the monitor was last checked, its ID and URL, its IP address, and a dedicated sparkline showing its check status over time. +[float] +=== Observability integrations + +[role="screenshot"] +image::uptime/images/observability_integrations.png[Observability integrations] + +The Monitor list also contains a menu of possible integrations. If Uptime detects Kubernetes or +Docker related host information, it will provide links to open the Infrastructure UI or Logs UI pre-filtered +for this host. Additionally, this feature supplies links to simply filter the other views on the host's +IP address, to help you quickly determine if these other solutions contain data relevant to your current +interest. + [float] === Error list diff --git a/docs/visualize/xychart.asciidoc b/docs/visualize/xychart.asciidoc index dd8e0c33ecd92..816efdef5b0b4 100644 --- a/docs/visualize/xychart.asciidoc +++ b/docs/visualize/xychart.asciidoc @@ -62,7 +62,11 @@ Style all the Y-axes of the chart. *Labels - Rotate*:::: You can enter the number in degrees for how much you want to rotate labels *Labels - Truncate*:::: You can enter the size in pixels to which the label is truncated *Scale to Data Bounds*:::: The default Y-axis bounds are zero and the maximum value returned in the data. Check - this box to change both upper and lower bounds to match the values returned in the data. + this box to change both upper and lower bounds to match the values returned in the data. + Checking this option may cause that the bar, which value equals to the lower bounds/ + upper bounds (in case only negative values are depicted) is hidden. + To avoid that, you can define bounds margin. Via bounds margin you specify a value, + which decreases/increases the lower/upper bounds when displaying the plot. *Custom Extents*:::: You can define custom minimum and maximum for each axis [float] diff --git a/package.json b/package.json index 5ae8763ffd181..40aae96fe054e 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "@babel/core": "7.4.5", "@babel/polyfill": "7.4.4", "@babel/register": "7.4.4", - "@elastic/charts": "^4.2.6", + "@elastic/charts": "^6.0.1", "@elastic/datemath": "5.0.2", "@elastic/eui": "11.3.2", "@elastic/filesaver": "1.1.2", @@ -137,7 +137,7 @@ "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", - "cache-loader": "4.0.0", + "cache-loader": "1.2.2", "chalk": "^2.4.1", "color": "1.0.3", "commander": "2.20.0", @@ -263,7 +263,7 @@ "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/types": "7.4.4", "@elastic/eslint-config-kibana": "0.15.0", - "@elastic/github-checks-reporter": "0.0.15", + "@elastic/github-checks-reporter": "0.0.19", "@elastic/makelogs": "^4.4.0", "@kbn/es": "1.0.0", "@kbn/eslint-import-resolver-kibana": "2.0.0", @@ -354,9 +354,9 @@ "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", - "enzyme": "^3.9.0", - "enzyme-adapter-react-16": "^1.13.1", - "enzyme-adapter-utils": "^1.10.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.14.0", + "enzyme-adapter-utils": "^1.12.0", "enzyme-to-json": "^3.3.4", "eslint": "5.16.0", "eslint-config-prettier": "4.3.0", diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/package.json b/packages/kbn-pm/src/production/integration_tests/__fixtures__/package.json deleted file mode 100644 index 60e84ee218150..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "kibana", - "version": "1.0.0", - "private": true, - "dependencies": { - "@elastic/foo": "link:packages/foo", - "@elastic/baz": "link:packages/baz" - }, - "devDependencies": { - - }, - "scripts": { - "build": "echo 'should not be called'; false" - } -} \ No newline at end of file diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/bar/package.json b/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/bar/package.json deleted file mode 100644 index c1269fc7c037b..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/bar/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@elastic/bar", - "version": "1.0.0", - "private": true, - "main": "./target/index.js", - "devDependencies": { - "@babel/cli": "^7.2.3", - "@babel/core": "^7.3.4", - "@babel/preset-env": "^7.3.4" - }, - "scripts": { - "build": "babel --presets=@babel/preset-env --out-dir target src" - } -} diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/build/package.json b/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/build/package.json deleted file mode 100644 index cab90026281ed..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/build/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@elastic/baz", - "version": "1.0.0", - "main": "./index.js" -} diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/package.json b/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/package.json deleted file mode 100644 index f33caad0824e9..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@elastic/baz", - "version": "1.0.0", - "private": true, - "main": "./code.js", - "dependencies": { - "@elastic/quux": "link:../quux", - "noop3": "999.999.999" - }, - "devDependencies": { - "shx": "^0.2.2" - }, - "scripts": { - "build": "shx cp code.js build/index.js" - }, - "kibana": { - "build": { - "intermediateBuildDirectory": "build" - } - } -} diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/foo/package.json b/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/foo/package.json deleted file mode 100644 index 58421e2a334f3..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/foo/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@elastic/foo", - "version": "1.0.0", - "private": true, - "main": "./target/index.js", - "kibana": { - "build": { - "oss": false - } - }, - "dependencies": { - "@elastic/bar": "link:../bar" - }, - "devDependencies": { - "@babel/core": "^7.3.4", - "@babel/cli": "^7.2.3", - "@babel/preset-env": "^7.3.4", - "moment": "2.20.1" - }, - "scripts": { - "build": "babel --presets=@babel/preset-env --out-dir target src" - } -} diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/quux/package.json b/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/quux/package.json deleted file mode 100644 index 6a82d3fa1f932..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/quux/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@elastic/quux", - "version": "1.0.0", - "private": true, - "main": "./quux.js", - "devDependencies": { - "shx": "^0.2.2" - }, - "scripts": { - "build": "shx mkdir build; shx cp quux.js build/index.js" - }, - "kibana": { - "build": { - "intermediateBuildDirectory": "build" - } - } -} diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/shouldskip/package.json b/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/shouldskip/package.json deleted file mode 100644 index ee7406d9ac858..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/shouldskip/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@elastic/shouldskip", - "version": "1.0.0", - "private": true, - "main": "./target/index.js", - "dependencies": { - "@elastic/bar": "link:../bar" - }, - "scripts": { - "build": "echo 'should not be called'; false" - } -} \ No newline at end of file diff --git a/packages/kbn-pm/src/production/integration_tests/__snapshots__/build_production_projects.test.ts.snap b/packages/kbn-pm/src/production/integration_tests/__snapshots__/build_production_projects.test.ts.snap deleted file mode 100644 index cae2ad911c4e3..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/__snapshots__/build_production_projects.test.ts.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`kbn-pm production builds and copies only OSS projects for production 1`] = ` -Array [ - "packages/bar/package.json", - "packages/bar/src/index.js", - "packages/bar/target/index.js", - "packages/bar/yarn.lock", - "packages/baz/index.js", - "packages/baz/package.json", - "packages/quux/index.js", - "packages/quux/package.json", -] -`; - -exports[`kbn-pm production builds and copies projects for production 1`] = ` -Array [ - "packages/bar/package.json", - "packages/bar/src/index.js", - "packages/bar/target/index.js", - "packages/bar/yarn.lock", - "packages/baz/index.js", - "packages/baz/package.json", - "packages/foo/package.json", - "packages/foo/src/index.js", - "packages/foo/target/index.js", - "packages/foo/yarn.lock", - "packages/quux/index.js", - "packages/quux/package.json", -] -`; - -exports[`kbn-pm production builds and copies projects for production: packages/bar/package.json 1`] = ` -Object { - "devDependencies": Object { - "@babel/cli": "^7.2.3", - "@babel/core": "^7.3.4", - "@babel/preset-env": "^7.3.4", - }, - "main": "./target/index.js", - "name": "@elastic/bar", - "private": true, - "scripts": Object { - "build": "babel --presets=@babel/preset-env --out-dir target src", - }, - "version": "1.0.0", -} -`; - -exports[`kbn-pm production builds and copies projects for production: packages/baz/package.json 1`] = ` -Object { - "main": "./index.js", - "name": "@elastic/baz", - "version": "1.0.0", -} -`; - -exports[`kbn-pm production builds and copies projects for production: packages/foo/package.json 1`] = ` -Object { - "dependencies": Object { - "@elastic/bar": "link:../bar", - }, - "devDependencies": Object { - "@babel/cli": "^7.2.3", - "@babel/core": "^7.3.4", - "@babel/preset-env": "^7.3.4", - "moment": "2.20.1", - }, - "kibana": Object { - "build": Object { - "oss": false, - }, - }, - "main": "./target/index.js", - "name": "@elastic/foo", - "private": true, - "scripts": Object { - "build": "babel --presets=@babel/preset-env --out-dir target src", - }, - "version": "1.0.0", -} -`; - -exports[`kbn-pm production builds and copies projects for production: packages/quux/package.json 1`] = ` -Object { - "devDependencies": Object { - "shx": "^0.2.2", - }, - "kibana": Object { - "build": Object { - "intermediateBuildDirectory": "build", - }, - }, - "main": "./quux.js", - "name": "@elastic/quux", - "private": true, - "scripts": Object { - "build": "shx mkdir build; shx cp quux.js build/index.js", - }, - "version": "1.0.0", -} -`; diff --git a/packages/kbn-pm/src/production/integration_tests/build_production_projects.test.ts b/packages/kbn-pm/src/production/integration_tests/build_production_projects.test.ts deleted file mode 100644 index dd313a79504be..0000000000000 --- a/packages/kbn-pm/src/production/integration_tests/build_production_projects.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import copy from 'cpy'; -import globby from 'globby'; -import { join, resolve } from 'path'; -import tempy from 'tempy'; - -import { readPackageJson } from '../../utils/package_json'; -import { getProjects } from '../../utils/projects'; -import { buildProductionProjects } from '../build_production_projects'; - -describe('kbn-pm production', () => { - let tmpDir: string; - let buildRoot: string; - - const timeout = 1 * 60 * 1000; - - beforeEach(async () => { - tmpDir = tempy.directory(); - buildRoot = tempy.directory(); - const fixturesPath = resolve(__dirname, '__fixtures__'); - - // Copy all the test fixtures into a tmp dir, as we will be mutating them - await copy(['**/*'], tmpDir, { - cwd: fixturesPath, - dot: true, - nodir: true, - parents: true, - }); - - const projects = await getProjects(tmpDir, ['.', './packages/*']); - - for (const project of projects.values()) { - // This will both install dependencies and generate `yarn.lock` files - await project.installDependencies({ - extraArgs: ['--silent', '--no-progress'], - }); - } - }, timeout); - - test( - 'builds and copies projects for production', - async () => { - await buildProductionProjects({ kibanaRoot: tmpDir, buildRoot }); - - const files = await globby(['**/*', '!**/node_modules/**'], { - cwd: buildRoot, - }); - - expect(files.sort()).toMatchSnapshot(); - - for (const file of files) { - if (file.endsWith('package.json')) { - expect(await readPackageJson(join(buildRoot, file))).toMatchSnapshot(file); - } - } - }, - timeout - ); - - test( - 'builds and copies only OSS projects for production', - async () => { - await buildProductionProjects({ kibanaRoot: tmpDir, buildRoot, onlyOSS: true }); - - const files = await globby(['**/*', '!**/node_modules/**'], { - cwd: buildRoot, - }); - - expect(files.sort()).toMatchSnapshot(); - }, - timeout - ); -}); diff --git a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js index 26a1509d5c4ec..a5744d6498801 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js +++ b/packages/kbn-test/src/functional_tests/lib/run_kibana_server.js @@ -31,7 +31,7 @@ export async function runKibanaServer({ procs, config, options }) { ...process.env, }, cwd: installDir || KIBANA_ROOT, - wait: /Server running/, + wait: /http server running/, }); } diff --git a/rfcs/text/0003_handler_interface.md b/rfcs/text/0003_handler_interface.md new file mode 100644 index 0000000000000..51e78cf7c9f54 --- /dev/null +++ b/rfcs/text/0003_handler_interface.md @@ -0,0 +1,356 @@ +- Start Date: 2019-05-11 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +# Summary + +Handlers are asynchronous functions registered with core services invoked to +respond to events like a HTTP request, or mounting an application. _Handler +context_ is a pattern that would allow APIs and values to be provided to handler +functions by the service that owns the handler (aka service owner) or other +services that are not necessarily known to the service owner. + +# Basic example + +```js +// services can register context providers to route handlers +http.registerContext('myApi', (context, request) => ({ getId() { return request.params.myApiId } })); + +http.router.route({ + method: 'GET', + path: '/saved_object/:id', + // routeHandler implements the "handler" interface + async routeHandler(context, request) { + // returned value of the context registered above is exposed on the `myApi` key of context + const objectId = context.myApi.getId(); + // core context is always present in the `context.core` key + return context.core.savedObjects.find(objectId); + }, +}); +``` + +# Motivation + +The informal concept of handlers already exists today in HTTP routing, task +management, and the designs of application mounting and alert execution. +Examples: + +```tsx +// Task manager tasks +taskManager.registerTaskDefinitions({ + myTask: { + title: 'The task', + timeout: '5m', + createTaskRunner(context) { + return { + async run() { + const docs = await context.core.elasticsearch.search(); + doSomethingWithDocs(docs); + } + } + } + } +}) + +// Application mount handlers +application.registerApp({ + id: 'myApp', + mount(context, domElement) { + ReactDOM.render( + , + domElement + ); + return () => ReactDOM.unmountComponentAtNode(domElement); + } +}); + +// Alerting +alerting.registerType({ + id: 'myAlert', + async execute(context, params, state) { + const indexPatterns = await context.core.savedObjects.find('indexPattern'); + // use index pattern to search + } +}) +``` + +Without a formal definition, each handler interface varies slightly and +different solutions are developed per handler for managing complexity and +enabling extensibility. + +The official handler context convention seeks to address five key problems: + +1. Different services and plugins should be able to expose functionality that + is configured for the particular context where the handler is invoked, such + as a savedObject client in an alert handler already being configured to use + the appropriate API token. + +2. The service owner of a handler should not need to know about the services + or plugins that extend its handler context, such as the security plugin + providing a currentUser function to an HTTP router handler. + +3. Functionality in a handler should be "fixed" for the life of that + handler's context rather than changing configuration under the hood in + mid-execution. For example, while Elasticsearch clients can technically + be replaced throughout the course of the Kibana process, an HTTP route + handler should be able to depend on their being a consistent client for its + own shorter lifespan. + +4. Plugins should not need to pass down high level service contracts throughout + their business logic just so they can access them within the context of a + handler. + +5. Functionality provided by services should not be arbitrarily used in + unconstrained execution such as in the plugin lifecycle hooks. For example, + it's appropriate for an Elasticsearch client to throw an error if it's used + inside an API route and Elasticsearch isn't available, however it's not + appropriate for a plugin to throw an error in their start function if + Elasticsearch is not available. If the ES client was only made available + within the handler context and not to the plugin's start contract at large, + then this isn't an issue we'll encounter in the first place. + +# Detailed design + +There are two parts to this proposal. The first is the handler interface +itself, and the second is the interface that a service owner implements to make +their handlers extensible. + +## Handler Context + +```ts +interface Context { + core: Record; + [contextName: string]: unknown; +} + +type Handler = (context: Context, ...args: unknown[]) => Promise; +``` + +- `args` in this example is specific to the handler type, for instance in a + http route handler, this would include the incoming request object. +- The context object is marked as `Partial` because the contexts + available will vary depending on which plugins are enabled. +- This type is a convention, not a concrete type. The `core` key should have a + known interface that is declared in the service owner's specific Context type. + +## Registering new contexts + +```ts +type ContextProvider = ( + context: Partial, + ...args: unknown[] +) => Promise; + +interface HandlerService { + registerContext(contextName: T, provider: ContextProvider): void; +} +``` + +- `args` in this example is specific to the handler type, for instance in a http + route handler, this would include the incoming request object. It would not + include the results from the other context providers in order to keep + providers from having dependencies on one another. +- The `HandlerService` is defined as a literal interface in this document, but + in practice this interface is just a guide for the pattern of registering + context values. Certain services may have multiple different types of + handlers, so they may choose not to use the generic name `registerContext` in + favor of something more explicit. + +## Context creation + +Before a handler is executed, each registered context provider will be called +with the given arguments to construct a context object for the handler. Each +provider must return an object of the correct type. The return values of these +providers is merged into a single object where each key of the object is the +name of the context provider and the value is the return value of the provider. +Key facts about context providers: + +- **Context providers are executed in registration order.** Providers are + registered during the setup phase, which happens in topological dependency + order, which will cause the context providers to execute in the same order. + Providers can leverage this property to rely on the context of dependencies to + be present during the execution of its own providers. All context registered + by Core will be present during all plugin context provider executions. +- **Context providers may be executed with the different arguments from + handlers.** Each service owner should define what arguments are available to + context providers, however the context itself should never be an argument (see + point above). +- **Context providers cannot takeover the handler execution.** Context providers + cannot "intercept" handlers and return a different response. This is different + than traditional middleware. It should be noted that throwing an exception + will be bubbled up to the calling code and may prevent the handler from + getting executed at all. How the service owner handles that exception is + service-specific. +- **Values returned by context providers are expected to be valid for the entire + execution scope of the handler.** + +Here's a simple example of how a service owner could construct a context and +execute a handler: + +```js +const contextProviders = new Map()>; + +async function executeHandler(handler, request, toolkit) { + const newContext = {}; + for (const [contextName, provider] of contextProviders.entries()) { + newContext[contextName] = await provider(newContext, request, toolkit); + } + + return handler(context, request, toolkit); +} +``` + +## End to end example + +```js +http.router.registerRequestContext('elasticsearch', async (context, request) => { + const client = await core.elasticsearch.client$.toPromise(); + return client.child({ + headers: { authorization: request.headers.authorization }, + }); +}); + +http.router.route({ + path: '/foo', + async routeHandler(context) { + context.core.elasticsearch.search(); // === callWithRequest(request, 'search') + }, +}); +``` + +## Types + +While services that implement this pattern will not be able to define a static +type, plugins should be able to reopen a type to extend it with whatever context +it provides. This allows the `registerContext` function to be type-safe. +For example, if the HTTP service defined a setup type like this: + +```ts +// http_service.ts +interface RequestContext { + core: { + elasticsearch: ScopedClusterClient; + }; + [contextName: string]?: unknown; +} + +interface HttpSetup { + // ... + + registerRequestContext( + contextName: T, + provider: (context: Partial, request: Request) => RequestContext[T] | Promise + ): void; + + // ... +} +``` + +A consuming plugin could extend the `RequestContext` to be type-safe like this: + +```ts +// my_plugin/server/index.ts +import { RequestContext } from '../../core/server'; + +// The plugin *should* add a new property to the RequestContext interface from +// core to represent whatever type its context provider returns. This will be +// available to any module that imports this type and will ensure that the +// registered context provider returns the expected type. +declare module "../../core/server" { + interface RequestContext { + myPlugin?: { // should be optional because this plugin may be disabled. + getFoo(): string; + } + } +} + +class MyPlugin { + setup(core) { + // This will be type-safe! + core.http.registerRequestContext('myPlugin', (context, request) => ({ + getFoo() { return 'foo!' } + })) + } +}; +``` + +# Drawbacks + +- Since the context properties that are present change if plugins are disabled, + they are all marked as optional properties which makes consuming the context + type awkward. We can expose types at the core and plugin level, but consumers + of those types might need to define which properties are present manually to + match their required plugin dependencies. Example: + ```ts + type RequiredDependencies = 'data' | 'timepicker'; + type OptionalDependencies = 'telemetry'; + type MyPluginContext = Pick & + Pick & + Pick, OptionalDependencies>; + // => { core: {}, data: Data, timepicker: Timepicker, telemetry?: Telemetry }; + ``` + This could even be provided as a generic type: + ```ts + type AvailableContext + = Pick & Required> & Partial>; + type MyPluginContext = AvailableContext; + // => { core: {}, data: Data, timepicker: Timepicker, telemetry?: Telemetry }; + ``` +- Extending types with `declare module` merging is not a typical pattern for + developers and it's not immediately obvious that you need to do this to type + the `registerContext` function. We do already use this pattern with extending + Hapi and EUI though, so it's not completely foreign. +- The longer we wait to implement this, the more refactoring of newer code + we'll need to do to roll this out. +- It's a new formal concept and set of terminology that developers will need to + learn relative to other new platform terminology. +- Handlers are a common pattern for HTTP route handlers, but people don't + necessarily associate similar patterns elsewhere as the same set of problems. +- "Chicken and egg" questions will arise around where context providers should be + registered. For example, does the `http` service invoke its + registerRequestContext for `elasticsearch`, or does the `elasticsearch` service + invoke `http.registerRequestContext`, or does core itself register the + provider so neither service depends directly on the other. +- The existence of plugins that a given plugin does not depend on may leak + through the context object. This becomes a problem if a plugin uses any + context properties provided by a plugin that it does not depend on and that + plugin gets disabled in production. This can be solved by service owners, but + may need to be reimplemented for each one. + +# Alternatives + +The obvious alternative is what we've always done: expose all functionality at +the plugin level and then leave it up to the consumer to build a "context" for +their particular handler. This creates a lot of inconsistency and makes +creating simple but useful handlers more complicated. This can also lead to +subtle but significant bugs as it's unreasonable to assume all developers +understand the important details for constructing a context with plugins they +don't know anything about. + +# Adoption strategy + +The easiest adoption strategy to is to roll this change out in the new platform +before we expose any handlers to plugins, which means there wouldn't be any +breaking change. + +In the event that there's a long delay before this is implemented, its +principles can be rolled out without altering plugin lifecycle arguments so +existing handlers would continue to operate for a timeframe of our choosing. + +# How we teach this + +The handler pattern should be one we officially adopt in our developer +documentation alongside other new platform terminology. + +Core should be updated to follow this pattern once it is rolled out so there +are plenty of examples in the codebase. + +For many developers, the formalization of this interface will not have an +obvious, immediate impact on the code they're writing since the concept is +already widely in use in various forms. + +# Unresolved questions + +Is the term "handler" appropriate and sufficient? I also toyed with the phrase +"contextual handler" to make it a little more distinct of a concept. I'm open +to ideas here. diff --git a/rfcs/text/0004_application_service_mounting.md b/rfcs/text/0004_application_service_mounting.md new file mode 100644 index 0000000000000..30e8d9a05b8b4 --- /dev/null +++ b/rfcs/text/0004_application_service_mounting.md @@ -0,0 +1,327 @@ +- Start Date: 2019-05-10 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +# Summary + +A front-end service to manage registration and root-level routing for +first-class applications. + +# Basic example + + +```tsx +// my_plugin/public/application.js + +import React from 'react'; +import ReactDOM from 'react-dom'; + +import { MyApp } from './componnets'; + +export function renderApp(context, targetDomElement) { + ReactDOM.render( + , + targetDomElement + ); + + return () => { + ReactDOM.unmountComponentAtNode(targetDomElement); + }; +} +``` + +```tsx +// my_plugin/public/plugin.js + +class MyPlugin { + setup({ application }) { + application.register({ + id: 'my-app', + title: 'My Application', + async mount(context, targetDomElement) { + const { renderApp } = await import('./applcation'); + return renderApp(context, targetDomElement); + } + }); + } +} +``` + +# Motivation + +By having centralized management of applications we can have a true single page +application. It also gives us a single place to enforce authorization and/or +licensing constraints on application access. + +By making the mounting interface of the ApplicationService generic, we can +support many different rendering technologies simultaneously to avoid framework +lock-in. + +# Detailed design + +## Interface + +```ts +/** A context type that implements the Handler Context pattern from RFC-0003 */ +export interface MountContext { + /** This is the base path for setting up your router. */ + basename: string; + /** These services serve as an example, but are subject to change. */ + core: { + http: { + fetch(...): Promise; + }; + i18n: { + translate( + id: string, + defaultMessage: string, + values?: Record + ): string; + }; + notifications: { + toasts: { + add(...): void; + }; + }; + overlays: { + showFlyout(render: (domElement) => () => void): Flyout; + showModal(render: (domElement) => () => void): Modal; + }; + uiSettings: { ... }; + }; + /** Other plugins can inject context by registering additional context providers */ + [contextName: string]: unknown; +} + +export type Unmount = () => Promise | void; + +export interface AppSpec { + /** + * A unique identifier for this application. Used to build the route for this + * application in the browser. + */ + id: string; + + /** + * The title of the application. + */ + title: string; + + /** + * A mount function called when the user navigates to this app's route. + * @param context the `MountContext generated for this app + * @param targetDomElement An HTMLElement to mount the application onto. + * @returns An unmounting function that will be called to unmount the application. + */ + mount(context: MountContext, targetDomElement: HTMLElement): Unmount | Promise; + + /** + * A EUI iconType that will be used for the app's icon. This icon + * takes precendence over the `icon` property. + */ + euiIconType?: string; + + /** + * A URL to an image file used as an icon. Used as a fallback + * if `euiIconType` is not provided. + */ + icon?: string; + + /** + * Custom capabilities defined by the app. + */ + capabilities?: Partial; +} + +export interface ApplicationSetup { + /** + * Registers an application with the system. + */ + register(app: AppSpec): void; + registerMountContext( + contextName: T, + provider: (context: Partial) => MountContext[T] | Promise + ): void; +} + +export interface ApplicationStart { + /** + * The UI capabilities for the current user. + */ + capabilities: Capabilties; +} +``` + +## Mounting + +When an app is registered via `register`, it must provide a `mount` function +that will be invoked whenever the window's location has changed from another app +to this app. + +This function is called with a `MountContext` and an `HTMLElement` for the +application to render itself to. The mount function must also return a function +that can be called by the ApplicationService to unmount the application at the +given DOM node. The mount function may return a Promise of an unmount function +in order to import UI code dynamically. + +The ApplicationService's `register` method will only be available during the +*setup* lifecycle event. This allows the system to know when all applications +have been registered. + +The `mount` function will also get access to the `MountContext` that has many of +the same core services available during the `start` lifecycle. Plugins can also +register additional context attributes via the `registerMountContext` function. + +## Routing + +The ApplicationService will serve as the global frontend router for Kibana, +enabling Kibana to be a 100% single page application. However, the router will +only manage top-level routes. Applications themselves will need to implement +their own routing as subroutes of the top-level route. + +An example: +- "MyApp" is registered with `id: 'my-app'` +- User navigates from mykibana.com/app/home to mykibana.com/app/my-app +- ApplicationService sees the root app has changed and mounts the new + application: + - Calls the `Unmount` function returned my "Home"'s `mount` + - Calls the `mount` function registered by "MyApp" +- MyApp's internal router takes over rest of routing. Redirects to initial + "overview" page: mykibana.com/app/my-app/overview + +When setting up a router, your application should only handle the part of the +URL following the `context.basename` provided when you application is mounted. + +### Legacy Applications + +In order to introduce this service now, the ApplicationService will need to be +able to handle "routing" to legacy applications. We will not be able to run +multiple legacy applications on the same page load due to shared stateful +modules in `ui/public`. + +Instead, the ApplicationService should do a full-page refresh when rendering +legacy applications. Internally, this will be managed by registering legacy apps +with the ApplicationService separately and handling those top-level routes by +starting a full-page refresh rather than a mounting cycle. + +## Complete Example + +Here is a complete example that demonstrates rendering a React application with +a full-featured router and code-splitting. Note that using React or any other +3rd party tools featured here is not required to build a Kibana Application. + +```tsx +// my_plugin/public/application.ts + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, Route } from 'react-router-dom'; +import loadable from '@loadable/component'; + +// Apps can choose to load components statically in the same bundle or +// dynamically when routes are rendered. +import { HomePage } from './pages'; +const LazyDashboard = loadable(() => import('./pages/dashboard')); + +const MyApp = ({ basename }) => ( + // Setup router's basename from the basename provided from MountContext + + + {/* mykibana.com/app/my-app/ */} + + + {/* mykibana.com/app/my-app/dashboard/42 */} + } + /> + + , +); + +export function renderApp(context, targetDomElement) { + ReactDOM.render( + // `context.basename` would be `/app/my-app` in this example. + // This exact string is not guaranteed to be stable, always reference + // `context.basename`. + , + targetDomElem + ); + + return () => ReactDOM.unmountComponentAtNode(targetDomElem); +} +``` + +```tsx +// my_plugin/public/plugin.tsx + +export class MyPlugin { + setup({ application }) { + application.register({ + id: 'my-app', + async mount(context, targetDomElem) { + const { renderApp } = await import('./applcation'); + return renderApp(context, targetDomElement); + } + }); + } +} +``` + +## Core Entry Point + +Once we can support application routing for new and legacy applications, we +should create a new entry point bundle that only includes Core and any necessary +uiExports (hacks for example). This should be served by the backend whenever a +`/app/` request is received for an app that the legacy platform does not +have a bundle for. + +# Drawbacks + +- Implementing this will be significant work and requires migrating legacy code + from `ui/chrome` +- Making Kibana a single page application may lead to problems if applications + do not clean themselves up properly when unmounted +- Application `mount` functions will have access to *setup* via the closure. We + may want to lock down these APIs from being used after *setup* to encourage + usage of the `MountContext` instead. +- In order to support new applications being registered in the legacy platform, + we will need to create a new `uiExport` that is imported during the new + platform's *setup* lifecycle event. This is necessary because app registration + must happen prior to starting the legacy platform. This is only an issue for + plugins that are migrating using a shim in the legacy platform. + +# Alternatives + +- We could provide a full featured react-router instance that plugins could + plug directly into. The downside is this locks us more into React and makes + code splitting a bit more challenging. + +# Adoption strategy + +Adoption of the application service will have to happen as part of the migration +of each plugin. We should be able to support legacy plugins registering new +platform-style applications before they actually move all of their code +over to the new platform. + +# How we teach this + +Introducing this service makes applications a first-class feature of the Kibana +platform. Right now, plugins manage their own routes and can export "navlinks" +that get rendered in the navigation UI, however there is a not a self-contained +concept like an application to encapsulate these related responsibilities. It +will need to be emphasized that plugins can register zero, one, or multiple +applications. + +Most new and existing Kibana developers will need to understand how the +ApplicationService works and how multiple apps run in a single page application. +This should be accomplished through thorough documentation in the +ApplicationService's API implementation as well as in general plugin development +tutorials and documentation. + +# Unresolved questions + +- Are there any major caveats to having multiple routers on the page? If so, how +can these be prevented or worked around? +- How should global URL state be shared across applications, such as timepicker +state? diff --git a/src/cli/serve/integration_tests/reload_logging_config.test.js b/src/cli/serve/integration_tests/reload_logging_config.test.js index fbb8fd0431e84..2b6f229ca9dae 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ b/src/cli/serve/integration_tests/reload_logging_config.test.js @@ -181,7 +181,7 @@ describe('Server logging configuration', function () { '--logging.json', 'false' ]); - watchFileUntil(logPath, /Server running at/, 2 * minute) + watchFileUntil(logPath, /http server running/, 2 * minute) .then(() => { // once the server is running, archive the log file and issue SIGHUP fs.renameSync(logPath, logPathArchived); diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 957f542a53c6b..c97967afce6eb 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -210,6 +210,11 @@ describe('#start()', () => { expect(MockApplicationService.start).toHaveBeenCalledTimes(1); }); + it('calls uiSettings#start()', async () => { + await startCore(); + expect(MockUiSettingsService.start).toHaveBeenCalledTimes(1); + }); + it('calls i18n#start()', async () => { await startCore(); expect(MockI18nService.start).toHaveBeenCalledTimes(1); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index cefd5e49fcfed..61605f6535023 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -176,6 +176,7 @@ export class CoreSystem { injectedMetadata, notifications, }); + const uiSettings = await this.uiSettings.start(); const core: InternalCoreStart = { application, @@ -185,6 +186,7 @@ export class CoreSystem { injectedMetadata, notifications, overlays, + uiSettings, }; const plugins = await this.plugins.start(core); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 30e43c9809b37..59cad5f8109c3 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -57,7 +57,7 @@ import { } from './notifications'; import { OverlayRef, OverlayStart } from './overlays'; import { Plugin, PluginInitializer, PluginInitializerContext } from './plugins'; -import { UiSettingsClient, UiSettingsSetup, UiSettingsState } from './ui_settings'; +import { UiSettingsClient, UiSettingsSetup, UiSettingsStart, UiSettingsState } from './ui_settings'; import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; export { CoreContext, CoreSystem } from './core_system'; @@ -105,6 +105,8 @@ export interface CoreStart { notifications: NotificationsStart; /** {@link OverlayStart} */ overlays: OverlayStart; + /** {@link UiSettingsStart} */ + uiSettings: UiSettingsStart; } /** @internal */ @@ -151,4 +153,5 @@ export { UiSettingsClient, UiSettingsState, UiSettingsSetup, + UiSettingsStart, }; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 70524b2a16943..74dafd074dfe5 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -90,6 +90,7 @@ const i18nStart = i18nServiceMock.createStartContract(); const injectedMetadataStart = injectedMetadataServiceMock.createStartContract(); const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); +const uiSettingsStart = uiSettingsServiceMock.createStartContract(); const defaultStartDeps = { core: { @@ -100,6 +101,7 @@ const defaultStartDeps = { injectedMetadata: injectedMetadataStart, notifications: notificationsStart, overlays: overlayStart, + uiSettings: uiSettingsStart, }, targetDomElement: document.createElement('div'), plugins: {}, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index e59ec42b10b14..95ca6bf40ebe7 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -93,5 +93,6 @@ export function createPluginStartContext { injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), overlays: overlayServiceMock.createStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract() as jest.Mocked, }; mockStartContext = { ...omit(mockStartDeps, 'injectedMetadata'), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index ed5fbb26eba16..4740aa2996081 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -127,6 +127,8 @@ export interface CoreStart { notifications: NotificationsStart; // (undocumented) overlays: OverlayStart; + // (undocumented) + uiSettings: UiSettingsStart; } // @internal @@ -395,6 +397,9 @@ export class UiSettingsClient { // @public (undocumented) export type UiSettingsSetup = UiSettingsClient; +// @public (undocumented) +export type UiSettingsStart = UiSettingsClient; + // @public (undocumented) export interface UiSettingsState { // Warning: (ae-forgotten-export) The symbol "InjectedUiSettingsDefault" needs to be exported by the entry point index.d.ts diff --git a/src/core/public/ui_settings/index.ts b/src/core/public/ui_settings/index.ts index 6dab6f4a19c92..7ec0014ebcfd9 100644 --- a/src/core/public/ui_settings/index.ts +++ b/src/core/public/ui_settings/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export { UiSettingsService, UiSettingsSetup } from './ui_settings_service'; +export { UiSettingsService, UiSettingsSetup, UiSettingsStart } from './ui_settings_service'; export { UiSettingsClient } from './ui_settings_client'; export { UiSettingsState } from './types'; diff --git a/src/core/public/ui_settings/ui_settings_service.mock.ts b/src/core/public/ui_settings/ui_settings_service.mock.ts index 153251623de7f..c15efe671af3e 100644 --- a/src/core/public/ui_settings/ui_settings_service.mock.ts +++ b/src/core/public/ui_settings/ui_settings_service.mock.ts @@ -49,6 +49,7 @@ type UiSettingsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), + start: jest.fn(), stop: jest.fn(), }; @@ -59,4 +60,5 @@ const createMock = () => { export const uiSettingsServiceMock = { create: createMock, createSetupContract: createSetupContractMock, + createStartContract: createSetupContractMock, }; diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts index 03b51ae5f6be6..94e5e6e2418be 100644 --- a/src/core/public/ui_settings/ui_settings_service.test.ts +++ b/src/core/public/ui_settings/ui_settings_service.test.ts @@ -54,6 +54,15 @@ describe('#setup', () => { }); }); +describe('#start', () => { + it('returns an instance of UiSettingsClient', () => { + const uiSettings = new UiSettingsService(); + uiSettings.setup(defaultDeps); + const start = uiSettings.start(); + expect(start).toBeInstanceOf(MockUiSettingsClient); + }); +}); + describe('#stop', () => { it('runs fine if service never set up', () => { const service = new UiSettingsService(); diff --git a/src/core/public/ui_settings/ui_settings_service.ts b/src/core/public/ui_settings/ui_settings_service.ts index ea287d888fa37..00d1e9ea2cd5f 100644 --- a/src/core/public/ui_settings/ui_settings_service.ts +++ b/src/core/public/ui_settings/ui_settings_service.ts @@ -49,6 +49,10 @@ export class UiSettingsService { return this.uiSettingsClient; } + public start(): UiSettingsStart { + return this.uiSettingsClient!; + } + public stop() { if (this.uiSettingsClient) { this.uiSettingsClient.stop(); @@ -62,3 +66,6 @@ export class UiSettingsService { /** @public */ export type UiSettingsSetup = UiSettingsClient; + +/** @public */ +export type UiSettingsStart = UiSettingsClient; diff --git a/src/core/server/elasticsearch/cluster_client.test.ts b/src/core/server/elasticsearch/cluster_client.test.ts index de28818072bcf..db277fa0e0607 100644 --- a/src/core/server/elasticsearch/cluster_client.test.ts +++ b/src/core/server/elasticsearch/cluster_client.test.ts @@ -29,6 +29,7 @@ import { errors } from 'elasticsearch'; import { get } from 'lodash'; import { Logger } from '../logging'; import { loggingServiceMock } from '../logging/logging_service.mock'; +import { httpServerMock } from '../http/http_server.mocks'; import { ClusterClient } from './cluster_client'; const logger = loggingServiceMock.create(); @@ -241,7 +242,9 @@ describe('#asScoped', () => { }); test('creates additional Elasticsearch client only once', () => { - const firstScopedClusterClient = clusterClient.asScoped({ headers: { one: '1' } }); + const firstScopedClusterClient = clusterClient.asScoped( + httpServerMock.createRawRequest({ headers: { one: '1' } }) + ); expect(firstScopedClusterClient).toBeDefined(); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); @@ -257,7 +260,9 @@ describe('#asScoped', () => { jest.clearAllMocks(); - const secondScopedClusterClient = clusterClient.asScoped({ headers: { two: '2' } }); + const secondScopedClusterClient = clusterClient.asScoped( + httpServerMock.createRawRequest({ headers: { two: '2' } }) + ); expect(secondScopedClusterClient).toBeDefined(); expect(secondScopedClusterClient).not.toBe(firstScopedClusterClient); @@ -270,7 +275,7 @@ describe('#asScoped', () => { clusterClient = new ClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); - clusterClient.asScoped({ headers: { one: '1' } }); + clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger, { @@ -283,7 +288,7 @@ describe('#asScoped', () => { clusterClient = new ClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); - clusterClient.asScoped({ headers: { one: '1' } }); + clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger, { @@ -296,7 +301,7 @@ describe('#asScoped', () => { clusterClient = new ClusterClient(mockEsConfig, mockLogger); mockParseElasticsearchClientConfig.mockClear(); - clusterClient.asScoped({ headers: { one: '1' } }); + clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); expect(mockParseElasticsearchClientConfig).toHaveBeenCalledTimes(1); expect(mockParseElasticsearchClientConfig).toHaveBeenLastCalledWith(mockEsConfig, mockLogger, { @@ -306,7 +311,9 @@ describe('#asScoped', () => { }); test('passes only filtered headers to the scoped cluster client', () => { - clusterClient.asScoped({ headers: { zero: '0', one: '1', two: '2', three: '3' } }); + clusterClient.asScoped( + httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) + ); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); expect(MockScopedClusterClient).toHaveBeenCalledWith( @@ -317,7 +324,9 @@ describe('#asScoped', () => { }); test('both scoped and internal API caller fail if cluster client is closed', async () => { - clusterClient.asScoped({ headers: { zero: '0', one: '1', two: '2', three: '3' } }); + clusterClient.asScoped( + httpServerMock.createRawRequest({ headers: { zero: '0', one: '1', two: '2', three: '3' } }) + ); clusterClient.close(); @@ -330,6 +339,70 @@ describe('#asScoped', () => { `"Cluster client cannot be used after it has been closed."` ); }); + + test('does not fail when scope to not defined request', async () => { + clusterClient = new ClusterClient(mockEsConfig, mockLogger); + clusterClient.asScoped(); + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + {} + ); + }); + + test('does not fail when scope to a request without headers', async () => { + clusterClient = new ClusterClient(mockEsConfig, mockLogger); + clusterClient.asScoped({} as any); + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + {} + ); + }); + + test('calls getAuthHeaders and filters results for a real request', async () => { + clusterClient = new ClusterClient(mockEsConfig, mockLogger, () => ({ one: '1', three: '3' })); + clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { two: '2' } })); + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + { one: '1', two: '2' } + ); + }); + + test('getAuthHeaders results rewrite extends a request headers', async () => { + clusterClient = new ClusterClient(mockEsConfig, mockLogger, () => ({ one: 'foo' })); + clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1', two: '2' } })); + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + { one: 'foo', two: '2' } + ); + }); + + test("doesn't call getAuthHeaders for a fake request", async () => { + const getAuthHeaders = jest.fn(); + clusterClient = new ClusterClient(mockEsConfig, mockLogger, getAuthHeaders); + clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); + + expect(getAuthHeaders).not.toHaveBeenCalled(); + }); + + test('filters a fake request headers', async () => { + clusterClient = new ClusterClient(mockEsConfig, mockLogger); + clusterClient.asScoped({ headers: { one: '1', two: '2', three: '3' } }); + + expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); + expect(MockScopedClusterClient).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function), + { one: '1', two: '2' } + ); + }); }); describe('#close', () => { @@ -359,7 +432,7 @@ describe('#close', () => { }); test('closes both internal and scoped underlying Elasticsearch clients', () => { - clusterClient.asScoped({ headers: { one: '1' } }); + clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); expect(mockEsClientInstance.close).not.toHaveBeenCalled(); expect(mockScopedEsClientInstance.close).not.toHaveBeenCalled(); @@ -370,7 +443,7 @@ describe('#close', () => { }); test('does not call close on already closed client', () => { - clusterClient.asScoped({ headers: { one: '1' } }); + clusterClient.asScoped(httpServerMock.createRawRequest({ headers: { one: '1' } })); clusterClient.close(); mockEsClientInstance.close.mockClear(); diff --git a/src/core/server/elasticsearch/cluster_client.ts b/src/core/server/elasticsearch/cluster_client.ts index 45e3f0a20c0c4..73b50f82f7500 100644 --- a/src/core/server/elasticsearch/cluster_client.ts +++ b/src/core/server/elasticsearch/cluster_client.ts @@ -20,7 +20,10 @@ import Boom from 'boom'; import { Client } from 'elasticsearch'; import { get } from 'lodash'; -import { filterHeaders, Headers } from '../http/router'; +import { Request } from 'hapi'; + +import { GetAuthHeaders, isRealRequest } from '../http'; +import { filterHeaders, KibanaRequest, ensureRawRequest } from '../http/router'; import { Logger } from '../logging'; import { ElasticsearchClientConfig, @@ -28,6 +31,15 @@ import { } from './elasticsearch_client_config'; import { ScopedClusterClient } from './scoped_cluster_client'; +/** + * Support Legacy platform request for the period of migration. + * + * @public + */ + +export type LegacyRequest = Request; + +const noop = () => undefined; /** * The set of options that defines how API call should be made and result be * processed. @@ -95,6 +107,15 @@ async function callAPI( } } +/** + * Fake request object created manually by Kibana plugins. + * @public + */ +export interface FakeRequest { + /** Headers used for authentication against Elasticsearch */ + headers: Record; +} + /** * Represents an Elasticsearch cluster API client and allows to call API on behalf * of the internal Kibana user and the actual user that is derived from the request @@ -119,7 +140,11 @@ export class ClusterClient { */ private isClosed = false; - constructor(private readonly config: ElasticsearchClientConfig, private readonly log: Logger) { + constructor( + private readonly config: ElasticsearchClientConfig, + private readonly log: Logger, + private readonly getAuthHeaders: GetAuthHeaders = noop + ) { this.client = new Client(parseElasticsearchClientConfig(config, log)); } @@ -163,9 +188,10 @@ export class ClusterClient { * scoped to the provided req. Consumers shouldn't worry about closing * scoped client instances, these will be automatically closed as soon as the * original cluster client isn't needed anymore and closed. - * @param req - Request the `ScopedClusterClient` instance will be scoped to. + * @param request - Request the `ScopedClusterClient` instance will be scoped to. + * Supports request optionality, Legacy.Request & FakeRequest for BWC with LegacyPlatform */ - public asScoped(req: { headers?: Headers } = {}) { + public asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest) { // It'd have been quite expensive to create and configure client for every incoming // request since it involves parsing of the config, reading of the SSL certificate and // key files etc. Moreover scoped client needs two Elasticsearch JS clients at the same @@ -181,11 +207,11 @@ export class ClusterClient { ); } - const headers = req.headers - ? filterHeaders(req.headers, this.config.requestHeadersWhitelist) - : req.headers; - - return new ScopedClusterClient(this.callAsInternalUser, this.callAsCurrentUser, headers); + return new ScopedClusterClient( + this.callAsInternalUser, + this.callAsCurrentUser, + filterHeaders(this.getHeaders(request), this.config.requestHeadersWhitelist) + ); } /** @@ -210,4 +236,16 @@ export class ClusterClient { throw new Error('Cluster client cannot be used after it has been closed.'); } } + + private getHeaders( + request?: KibanaRequest | LegacyRequest | FakeRequest + ): Record { + if (!isRealRequest(request)) { + return request && request.headers ? request.headers : {}; + } + const authHeaders = this.getAuthHeaders(request); + const headers = ensureRawRequest(request).headers; + + return { ...headers, ...authHeaders }; + } } diff --git a/src/core/server/elasticsearch/elasticsearch_service.test.ts b/src/core/server/elasticsearch/elasticsearch_service.test.ts index 901ab78130480..a0f7180129382 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.test.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.test.ts @@ -27,11 +27,15 @@ import { getEnvOptions } from '../config/__mocks__/env'; import { CoreContext } from '../core_context'; import { configServiceMock } from '../config/config_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; import { ElasticsearchConfig } from './elasticsearch_config'; import { ElasticsearchService } from './elasticsearch_service'; let elasticsearchService: ElasticsearchService; const configService = configServiceMock.create(); +const deps = { + http: httpServiceMock.createSetupContract(), +}; configService.atPath.mockReturnValue( new BehaviorSubject({ hosts: ['http://1.2.3.4'], @@ -54,7 +58,7 @@ afterEach(() => jest.clearAllMocks()); describe('#setup', () => { test('returns legacy Elasticsearch config as a part of the contract', async () => { - const setupContract = await elasticsearchService.setup(); + const setupContract = await elasticsearchService.setup(deps); await expect(setupContract.legacy.config$.pipe(first()).toPromise()).resolves.toBeInstanceOf( ElasticsearchConfig @@ -68,7 +72,7 @@ describe('#setup', () => { () => mockAdminClusterClientInstance ).mockImplementationOnce(() => mockDataClusterClientInstance); - const setupContract = await elasticsearchService.setup(); + const setupContract = await elasticsearchService.setup(deps); const [esConfig, adminClient, dataClient] = await combineLatest( setupContract.legacy.config$, @@ -85,12 +89,14 @@ describe('#setup', () => { expect(MockClusterClient).toHaveBeenNthCalledWith( 1, esConfig, - expect.objectContaining({ context: ['elasticsearch', 'admin'] }) + expect.objectContaining({ context: ['elasticsearch', 'admin'] }), + undefined ); expect(MockClusterClient).toHaveBeenNthCalledWith( 2, esConfig, - expect.objectContaining({ context: ['elasticsearch', 'data'] }) + expect.objectContaining({ context: ['elasticsearch', 'data'] }), + expect.any(Function) ); expect(mockAdminClusterClientInstance.close).not.toHaveBeenCalled(); @@ -98,7 +104,7 @@ describe('#setup', () => { }); test('returns `createClient` as a part of the contract', async () => { - const setupContract = await elasticsearchService.setup(); + const setupContract = await elasticsearchService.setup(deps); const mockClusterClientInstance = { close: jest.fn() }; MockClusterClient.mockImplementation(() => mockClusterClientInstance); @@ -110,7 +116,8 @@ describe('#setup', () => { expect(MockClusterClient).toHaveBeenCalledWith( mockConfig, - expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }) + expect.objectContaining({ context: ['elasticsearch', 'some-custom-type'] }), + expect.any(Function) ); }); }); @@ -123,7 +130,7 @@ describe('#stop', () => { () => mockAdminClusterClientInstance ).mockImplementationOnce(() => mockDataClusterClientInstance); - await elasticsearchService.setup(); + await elasticsearchService.setup(deps); await elasticsearchService.stop(); expect(mockAdminClusterClientInstance.close).toHaveBeenCalledTimes(1); diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index b3faab892bd97..4e90ff2e2baec 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -25,6 +25,7 @@ import { Logger } from '../logging'; import { ClusterClient } from './cluster_client'; import { ElasticsearchClientConfig } from './elasticsearch_client_config'; import { ElasticsearchConfig, ElasticsearchConfigType } from './elasticsearch_config'; +import { HttpServiceSetup, GetAuthHeaders } from '../http/'; /** @internal */ interface CoreClusterClients { @@ -33,6 +34,10 @@ interface CoreClusterClients { dataClient: ClusterClient; } +interface SetupDeps { + http: HttpServiceSetup; +} + /** @public */ export interface ElasticsearchServiceSetup { // Required for the BWC with the legacy Kibana only. @@ -58,7 +63,7 @@ export class ElasticsearchService implements CoreService new ElasticsearchConfig(rawConfig))); } - public async setup(): Promise { + public async setup(deps: SetupDeps): Promise { this.log.debug('Setting up elasticsearch service'); const clients$ = this.config$.pipe( @@ -78,7 +83,7 @@ export class ElasticsearchService implements CoreService clients.dataClient)), createClient: (type: string, clientConfig: ElasticsearchClientConfig) => { - return this.createClusterClient(type, clientConfig); + return this.createClusterClient(type, clientConfig, deps.http.auth.getAuthHeaders); }, }; } @@ -119,7 +124,15 @@ export class ElasticsearchService implements CoreService { + describe('stores authorization headers', () => { + it('retrieves a copy of headers associated with Kibana request', () => { + const headers = { authorization: 'token' }; + const storage = new AuthHeadersStorage(); + const rawRequest = httpServerMock.createRawRequest(); + storage.set(KibanaRequest.from(rawRequest), headers); + expect(storage.get(KibanaRequest.from(rawRequest))).toEqual(headers); + }); + + it('retrieves a copy of headers associated with Legacy.Request', () => { + const headers = { authorization: 'token' }; + const storage = new AuthHeadersStorage(); + const rawRequest = httpServerMock.createRawRequest(); + storage.set(rawRequest, headers); + expect(storage.get(rawRequest)).toEqual(headers); + }); + + it('retrieves a copy of headers associated with both KibanaRequest & Legacy.Request', () => { + const headers = { authorization: 'token' }; + const storage = new AuthHeadersStorage(); + const rawRequest = httpServerMock.createRawRequest(); + + storage.set(KibanaRequest.from(rawRequest), headers); + expect(storage.get(rawRequest)).toEqual(headers); + }); + }); +}); diff --git a/src/core/server/http/auth_headers_storage.ts b/src/core/server/http/auth_headers_storage.ts new file mode 100644 index 0000000000000..74d59952f5400 --- /dev/null +++ b/src/core/server/http/auth_headers_storage.ts @@ -0,0 +1,37 @@ +/* + * 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 { Request } from 'hapi'; +import { KibanaRequest, getIncomingMessage } from './router'; +import { AuthHeaders } from './lifecycle/auth'; + +/** + * Get headers to authenticate a user against Elasticsearch. + * @public + * */ +export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; + +export class AuthHeadersStorage { + private authHeadersCache = new WeakMap, AuthHeaders>(); + public set = (request: KibanaRequest | Request, headers: AuthHeaders) => { + this.authHeadersCache.set(getIncomingMessage(request), headers); + }; + public get: GetAuthHeaders = request => { + return this.authHeadersCache.get(getIncomingMessage(request)); + }; +} diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts index bd7bf1e62968c..3d82b43ffdd33 100644 --- a/src/core/server/http/auth_state_storage.ts +++ b/src/core/server/http/auth_state_storage.ts @@ -17,7 +17,7 @@ * under the License. */ import { Request } from 'hapi'; -import { KibanaRequest, toRawRequest } from './router'; +import { KibanaRequest, getIncomingMessage } from './router'; export enum AuthStatus { authenticated = 'authenticated', @@ -25,9 +25,6 @@ export enum AuthStatus { unknown = 'unknown', } -const getIncomingMessage = (request: KibanaRequest | Request) => - request instanceof KibanaRequest ? toRawRequest(request).raw.req : request.raw.req; - export class AuthStateStorage { private readonly storage = new WeakMap, unknown>(); constructor(private readonly canBeAuthenticated: () => boolean) {} diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index a6a868547bfb1..a327c6f090286 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -17,13 +17,10 @@ * under the License. */ import { Request } from 'hapi'; -import { KibanaRequest, toRawRequest } from './router'; +import { KibanaRequest, getIncomingMessage } from './router'; import { modifyUrl } from '../../utils'; -const getIncomingMessage = (request: KibanaRequest | Request) => - request instanceof KibanaRequest ? toRawRequest(request).raw.req : request.raw.req; - export class BasePath { private readonly basePathCache = new WeakMap, string>(); diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index f0cd50053cf14..559ab9137164f 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -20,7 +20,7 @@ import { Request, Server } from 'hapi'; import hapiAuthCookie from 'hapi-auth-cookie'; -import { KibanaRequest, toRawRequest } from './router'; +import { KibanaRequest, ensureRawRequest } from './router'; import { SessionStorageFactory, SessionStorage } from './session_storage'; export interface SessionStorageCookieOptions { @@ -31,7 +31,7 @@ export interface SessionStorageCookieOptions { } class ScopedCookieSessionStorage> implements SessionStorage { - constructor(private readonly server: Server, private readonly request: Readonly) {} + constructor(private readonly server: Server, private readonly request: Request) {} public async get(): Promise { try { return await this.server.auth.test('security-cookie', this.request as Request); @@ -73,9 +73,8 @@ export async function createCookieSessionStorageFactory( }); return { - asScoped(request: Readonly | KibanaRequest) { - const req = request instanceof KibanaRequest ? toRawRequest(request) : request; - return new ScopedCookieSessionStorage(server, req); + asScoped(request: KibanaRequest) { + return new ScopedCookieSessionStorage(server, ensureRawRequest(request)); }, }; } diff --git a/src/core/server/http/cookie_sesson_storage.test.ts b/src/core/server/http/cookie_sesson_storage.test.ts index 91ca1827d25b5..02ce240659a00 100644 --- a/src/core/server/http/cookie_sesson_storage.test.ts +++ b/src/core/server/http/cookie_sesson_storage.test.ts @@ -16,11 +16,34 @@ * specific language governing permissions and limitations * under the License. */ -import { Server } from 'hapi'; import request from 'request'; +import supertest from 'supertest'; +import { ByteSizeValue } from '@kbn/config-schema'; + +import { HttpServer } from './http_server'; +import { HttpConfig } from './http_config'; +import { Router } from './router'; +import { loggingServiceMock } from '../logging/logging_service.mock'; import { createCookieSessionStorageFactory } from './cookie_session_storage'; +let server: HttpServer; + +const logger = loggingServiceMock.create(); +const config = { + host: '127.0.0.1', + maxPayload: new ByteSizeValue(1024), + ssl: {}, +} as HttpConfig; + +beforeEach(() => { + server = new HttpServer(logger.get()); +}); + +afterEach(async () => { + await server.stop(); +}); + interface User { id: string; roles?: string[]; @@ -53,24 +76,25 @@ const cookieOptions = { describe('Cookie based SessionStorage', () => { describe('#set()', () => { it('Should write to session storage & set cookies', async () => { - const server = new Server(); - const factory = await createCookieSessionStorageFactory(server, cookieOptions); - server.route({ - method: 'GET', - path: '/set', - options: { - handler: (req, h) => { - const sessionStorage = factory.asScoped(req); - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); - return h.response(); - }, - }, + const router = new Router(''); + + router.get({ path: '/', validate: false }, (req, res) => { + const sessionStorage = factory.asScoped(req); + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return res.ok({}); }); - const response = await server.inject('/set'); - expect(response.statusCode).toBe(200); + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + const factory = await createCookieSessionStorageFactory(innerServer, cookieOptions); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200); - const cookies = response.headers['set-cookie']; + const cookies = response.get('set-cookie'); expect(cookies).toBeDefined(); expect(cookies).toHaveLength(1); @@ -84,100 +108,98 @@ describe('Cookie based SessionStorage', () => { }); describe('#get()', () => { it('Should read from session storage', async () => { - const server = new Server(); - const factory = await createCookieSessionStorageFactory(server, cookieOptions); - server.route({ - method: 'GET', - path: '/get', - options: { - handler: async (req, h) => { - const sessionStorage = factory.asScoped(req); - const sessionValue = await sessionStorage.get(); - if (!sessionValue) { - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); - return h.response(); - } - return h.response(sessionValue.value); - }, - }, + const router = new Router(''); + + router.get({ path: '/', validate: false }, async (req, res) => { + const sessionStorage = factory.asScoped(req); + const sessionValue = await sessionStorage.get(); + if (!sessionValue) { + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return res.ok({}); + } + return res.ok({ value: sessionValue.value }); }); - const response = await server.inject('/get'); - expect(response.statusCode).toBe(200); + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); - const cookies = response.headers['set-cookie']; + const factory = await createCookieSessionStorageFactory(innerServer, cookieOptions); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200); + + const cookies = response.get('set-cookie'); expect(cookies).toBeDefined(); expect(cookies).toHaveLength(1); const sessionCookie = retrieveSessionCookie(cookies[0]); - const response2 = await server.inject({ - method: 'GET', - url: '/get', - headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` }, - }); - expect(response2.statusCode).toBe(200); - expect(response2.result).toEqual(userData); + await supertest(innerServer.listener) + .get('/') + .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) + .expect(200, { value: userData }); }); it('Should return null for empty session', async () => { - const server = new Server(); - const factory = await createCookieSessionStorageFactory(server, cookieOptions); - server.route({ - method: 'GET', - path: '/get-empty', - options: { - handler: async (req, h) => { - const sessionStorage = factory.asScoped(req); - const sessionValue = await sessionStorage.get(); - return h.response(JSON.stringify(sessionValue)); - }, - }, + const router = new Router(''); + + router.get({ path: '/', validate: false }, async (req, res) => { + const sessionStorage = factory.asScoped(req); + const sessionValue = await sessionStorage.get(); + return res.ok({ value: sessionValue }); }); - const response = await server.inject('/get-empty'); - expect(response.statusCode).toBe(200); - expect(response.result).toBe('null'); - const cookies = response.headers['set-cookie']; + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + const factory = await createCookieSessionStorageFactory(innerServer, cookieOptions); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200, { value: null }); + + const cookies = response.get('set-cookie'); expect(cookies).not.toBeDefined(); }); it('Should return null for invalid session & clean cookies', async () => { - const server = new Server(); - const factory = await createCookieSessionStorageFactory(server, cookieOptions); + const router = new Router(''); + let setOnce = false; - server.route({ - method: 'GET', - path: '/get-invalid', - options: { - handler: async (req, h) => { - const sessionStorage = factory.asScoped(req); - if (!setOnce) { - setOnce = true; - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); - return h.response(); - } - const sessionValue = await sessionStorage.get(); - return h.response(JSON.stringify(sessionValue)); - }, - }, + router.get({ path: '/', validate: false }, async (req, res) => { + const sessionStorage = factory.asScoped(req); + if (!setOnce) { + setOnce = true; + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return res.ok({ value: userData }); + } + const sessionValue = await sessionStorage.get(); + return res.ok({ value: sessionValue }); }); - const response = await server.inject('/get-invalid'); - expect(response.statusCode).toBe(200); - const cookies = response.headers['set-cookie']; + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + const factory = await createCookieSessionStorageFactory(innerServer, cookieOptions); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200, { value: userData }); + + const cookies = response.get('set-cookie'); expect(cookies).toBeDefined(); await delay(sessionDurationMs); const sessionCookie = retrieveSessionCookie(cookies[0]); - const response2 = await server.inject({ - method: 'GET', - url: '/get-invalid', - headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` }, - }); - expect(response2.statusCode).toBe(200); - expect(response2.result).toBe('null'); + const response2 = await supertest(innerServer.listener) + .get('/') + .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) + .expect(200, { value: null }); - const cookies2 = response2.headers['set-cookie']; + const cookies2 = response2.get('set-cookie'); expect(cookies2).toEqual([ 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', ]); @@ -185,36 +207,37 @@ describe('Cookie based SessionStorage', () => { }); describe('#clear()', () => { it('Should clear session storage & remove cookies', async () => { - const server = new Server(); - const factory = await createCookieSessionStorageFactory(server, cookieOptions); - server.route({ - method: 'GET', - path: '/clear', - options: { - handler: async (req, h) => { - const sessionStorage = factory.asScoped(req); - if (await sessionStorage.get()) { - sessionStorage.clear(); - return h.response(); - } - sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); - return h.response(); - }, - }, + const router = new Router(''); + + router.get({ path: '/', validate: false }, async (req, res) => { + const sessionStorage = factory.asScoped(req); + if (await sessionStorage.get()) { + sessionStorage.clear(); + return res.ok({}); + } + sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); + return res.ok({}); }); - const response = await server.inject('/clear'); - const cookies = response.headers['set-cookie']; + const { registerRouter, server: innerServer } = await server.setup(config); + registerRouter(router); + + const factory = await createCookieSessionStorageFactory(innerServer, cookieOptions); + await server.start(); + + const response = await supertest(innerServer.listener) + .get('/') + .expect(200); + + const cookies = response.get('set-cookie'); const sessionCookie = retrieveSessionCookie(cookies[0]); - const response2 = await server.inject({ - method: 'GET', - url: '/clear', - headers: { cookie: `${sessionCookie.key}=${sessionCookie.value}` }, - }); - expect(response2.statusCode).toBe(200); + const response2 = await supertest(innerServer.listener) + .get('/') + .set('Cookie', `${sessionCookie.key}=${sessionCookie.value}`) + .expect(200); - const cookies2 = response2.headers['set-cookie']; + const cookies2 = response2.get('set-cookie'); expect(cookies2).toEqual([ 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', ]); diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 3676b4deaec46..88533504758be 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -62,6 +62,13 @@ test('listening after started', async () => { await server.start(); expect(server.isListening()).toBe(true); + expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(` +Array [ + Array [ + "http server running", + ], +] +`); }); test('200 OK with body', async () => { @@ -580,11 +587,10 @@ test('returns server and connection options on start', async () => { ...config, port: 12345, }; - const { options, server: innerServer } = await server.setup(configWithPort); + const { server: innerServer } = await server.setup(configWithPort); expect(innerServer).toBeDefined(); expect(innerServer).toBe((server as any).server); - expect(options).toMatchSnapshot(); }); test('registers registerOnPostAuth interceptor several times', async () => { @@ -639,7 +645,7 @@ describe('#registerAuth', () => { const user = { id: '42' }; const sessionStorage = sessionStorageFactory.asScoped(req); sessionStorage.set({ value: user, expires: Date.now() + 1000 }); - return t.authenticated(user); + return t.authenticated({ state: user }); }, cookieOptions); registerRouter(router); await server.start(); @@ -715,7 +721,7 @@ describe('#registerAuth', () => { }); }); - it(`allows manipulating cookies from route handler`, async () => { + it('allows manipulating cookies from route handler', async () => { const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); const { sessionStorageFactory } = await registerAuth((req, t) => { const user = { id: '42' }; @@ -749,6 +755,55 @@ describe('#registerAuth', () => { 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/', ]); }); + + it('is the only place with access to the authorization header', async () => { + const token = 'Basic: user:password'; + const { + registerAuth, + registerOnPreAuth, + registerOnPostAuth, + registerRouter, + server: innerServer, + } = await server.setup(config); + + let fromRegisterOnPreAuth; + await registerOnPreAuth((req, t) => { + fromRegisterOnPreAuth = req.getFilteredHeaders(['authorization']); + return t.next(); + }); + + let fromRegisterAuth; + await registerAuth((req, t) => { + fromRegisterAuth = req.getFilteredHeaders(['authorization']); + return t.authenticated(); + }, cookieOptions); + + let fromRegisterOnPostAuth; + await registerOnPostAuth((req, t) => { + fromRegisterOnPostAuth = req.getFilteredHeaders(['authorization']); + return t.next(); + }); + + let fromRouteHandler; + const router = new Router(''); + router.get({ path: '/', validate: false }, (req, res) => { + fromRouteHandler = req.getFilteredHeaders(['authorization']); + return res.ok({ content: 'ok' }); + }); + registerRouter(router); + + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .set('Authorization', token) + .expect(200); + + expect(fromRegisterOnPreAuth).toEqual({}); + expect(fromRegisterAuth).toEqual({ authorization: token }); + expect(fromRegisterOnPostAuth).toEqual({}); + expect(fromRouteHandler).toEqual({}); + }); }); test('enables auth for a route by default if registerAuth has been called', async () => { @@ -909,7 +964,7 @@ describe('#auth.get()', () => { const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config); const { sessionStorageFactory } = await registerAuth((req, t) => { sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 }); - return t.authenticated(user); + return t.authenticated({ state: user }); }, cookieOptions); const router = new Router(''); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 1cff8cd1d312e..eb571bdb47ddd 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Request, Server, ServerOptions } from 'hapi'; +import { Request, Server } from 'hapi'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; @@ -32,11 +32,11 @@ import { } from './cookie_session_storage'; import { SessionStorageFactory } from './session_storage'; import { AuthStateStorage } from './auth_state_storage'; +import { AuthHeadersStorage } from './auth_headers_storage'; import { BasePath } from './base_path_service'; export interface HttpServerSetup { server: Server; - options: ServerOptions; registerRouter: (router: Router) => void; /** * To define custom authentication and/or authorization mechanism for incoming requests. @@ -73,6 +73,7 @@ export interface HttpServerSetup { auth: { get: AuthStateStorage['get']; isAuthenticated: AuthStateStorage['isAuthenticated']; + getAuthHeaders: AuthHeadersStorage['get']; }; } @@ -83,9 +84,11 @@ export class HttpServer { private authRegistered = false; private readonly authState: AuthStateStorage; + private readonly authHeaders: AuthHeadersStorage; constructor(private readonly log: Logger) { this.authState = new AuthStateStorage(() => this.authRegistered); + this.authHeaders = new AuthHeadersStorage(); } public isListening() { @@ -110,7 +113,6 @@ export class HttpServer { this.setupBasePathRewrite(config, basePathService); return { - options: serverOptions, registerRouter: this.registerRouter.bind(this), registerOnPreAuth: this.registerOnPreAuth.bind(this), registerOnPostAuth: this.registerOnPostAuth.bind(this), @@ -120,6 +122,7 @@ export class HttpServer { auth: { get: this.authState.get, isAuthenticated: this.authState.isAuthenticated, + getAuthHeaders: this.authHeaders.get, }, // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't @@ -151,7 +154,8 @@ export class HttpServer { await this.server.start(); const serverPath = this.config!.rewriteBasePath || this.config!.basePath || ''; - this.log.debug(`http server running at ${this.server.info.uri}${serverPath}`); + this.log.info('http server running'); + this.log.debug(`http server listening on ${this.server.info.uri}${serverPath}`); } public async stop() { @@ -223,7 +227,13 @@ export class HttpServer { ); this.server.auth.scheme('login', () => ({ - authenticate: adoptToHapiAuthFormat(fn, this.authState.set), + authenticate: adoptToHapiAuthFormat(fn, (req, { state, headers }) => { + this.authState.set(req, state); + this.authHeaders.set(req, headers); + // we mutate headers only for the backward compatibility with the legacy platform. + // where some plugin read directly from headers to identify whether a user is authenticated. + Object.assign(req.headers, headers); + }), })); this.server.auth.strategy('session', 'login'); diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 07040560f89cd..2ea0614645c60 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Server, ServerOptions } from 'hapi'; +import { Server } from 'hapi'; import { HttpService } from './http_service'; import { HttpServerSetup } from './http_server'; import { HttpServiceSetup } from './http_service'; @@ -27,7 +27,6 @@ type ServiceSetupMockType = jest.Mocked & { }; const createSetupContractMock = () => { const setupContract: ServiceSetupMockType = { - options: ({} as unknown) as ServerOptions, // we can mock some hapi server method when we need it server: {} as Server, registerOnPreAuth: jest.fn(), @@ -43,6 +42,7 @@ const createSetupContractMock = () => { auth: { get: jest.fn(), isAuthenticated: jest.fn(), + getAuthHeaders: jest.fn(), }, createNewServer: jest.fn(), }; diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 16f946ffcc7ae..f003ba1314434 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -23,6 +23,7 @@ import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; import { HttpService, Router } from '.'; import { HttpConfigType, config } from './http_config'; +import { httpServerMock } from './http_server.mocks'; import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; @@ -43,6 +44,11 @@ const createConfigService = (value: Partial = {}) => { configService.setSchema(config.path, config.schema); return configService; }; +const fakeHapiServer = { + start: noop, + stop: noop, + route: noop, +}; afterEach(() => { jest.clearAllMocks(); @@ -56,9 +62,9 @@ test('creates and sets up http server', async () => { const httpServer = { isListening: () => false, - setup: jest.fn(), + setup: jest.fn().mockReturnValue({ server: fakeHapiServer }), start: jest.fn(), - stop: noop, + stop: jest.fn(), }; mockHttpServer.mockImplementation(() => httpServer); @@ -69,11 +75,62 @@ test('creates and sets up http server', async () => { expect(httpServer.setup).not.toHaveBeenCalled(); await service.setup(); - expect(httpServer.setup).toHaveBeenCalledTimes(1); + expect(httpServer.setup).toHaveBeenCalled(); expect(httpServer.start).not.toHaveBeenCalled(); await service.start(); - expect(httpServer.start).toHaveBeenCalledTimes(1); + expect(httpServer.start).toHaveBeenCalled(); +}); + +test('spins up notReady server until started if configured with `autoListen:true`', async () => { + const configService = createConfigService(); + const httpServer = { + isListening: () => false, + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + const notReadyHapiServer = { + start: jest.fn(), + stop: jest.fn(), + route: jest.fn(), + }; + + mockHttpServer + .mockImplementationOnce(() => httpServer) + .mockImplementationOnce(() => ({ + setup: () => ({ server: notReadyHapiServer }), + })); + + const service = new HttpService({ + configService, + env: new Env('.', getEnvOptions()), + logger, + }); + + await service.setup(); + + const mockResponse: any = { + code: jest.fn().mockImplementation(() => mockResponse), + header: jest.fn().mockImplementation(() => mockResponse), + }; + const mockResponseToolkit = { + response: jest.fn().mockReturnValue(mockResponse), + }; + + const [[{ handler }]] = notReadyHapiServer.route.mock.calls; + const response503 = await handler(httpServerMock.createRawRequest(), mockResponseToolkit); + expect(response503).toBe(mockResponse); + expect({ + body: mockResponseToolkit.response.mock.calls, + code: mockResponse.code.mock.calls, + header: mockResponse.header.mock.calls, + }).toMatchSnapshot('503 response'); + + await service.start(); + + expect(httpServer.start).toBeCalledTimes(1); + expect(notReadyHapiServer.stop).toBeCalledTimes(1); }); // this is an integration test! @@ -121,7 +178,7 @@ test('logs error if already set up', async () => { const httpServer = { isListening: () => true, - setup: jest.fn(), + setup: jest.fn().mockReturnValue({ server: fakeHapiServer }), start: noop, stop: noop, }; @@ -139,7 +196,7 @@ test('stops http server', async () => { const httpServer = { isListening: () => false, - setup: noop, + setup: jest.fn().mockReturnValue({ server: fakeHapiServer }), start: noop, stop: jest.fn(), }; @@ -157,13 +214,39 @@ test('stops http server', async () => { expect(httpServer.stop).toHaveBeenCalledTimes(1); }); +test('stops not ready server if it is running', async () => { + const configService = createConfigService(); + const mockHapiServer = { + start: jest.fn(), + stop: jest.fn(), + route: jest.fn(), + }; + const httpServer = { + isListening: () => false, + setup: jest.fn().mockReturnValue({ server: mockHapiServer }), + start: noop, + stop: jest.fn(), + }; + mockHttpServer.mockImplementation(() => httpServer); + + const service = new HttpService({ configService, env, logger }); + + await service.setup(); + + await service.stop(); + + expect(mockHapiServer.stop).toHaveBeenCalledTimes(1); +}); + test('register route handler', async () => { const configService = createConfigService(); const registerRouterMock = jest.fn(); const httpServer = { isListening: () => false, - setup: () => ({ registerRouter: registerRouterMock }), + setup: jest + .fn() + .mockReturnValue({ server: fakeHapiServer, registerRouter: registerRouterMock }), start: noop, stop: noop, }; @@ -181,10 +264,7 @@ test('register route handler', async () => { test('returns http server contract on setup', async () => { const configService = createConfigService(); - const httpServer = { - server: {}, - options: { someOption: true }, - }; + const httpServer = { server: fakeHapiServer, options: { someOption: true } }; mockHttpServer.mockImplementation(() => ({ isListening: () => false, diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index fec3774e2f366..a056300f6ed7e 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -19,6 +19,7 @@ import { Observable, Subscription } from 'rxjs'; import { first, map } from 'rxjs/operators'; +import { Server } from 'hapi'; import { LoggerFactory } from '../logging'; import { CoreService } from '../../types'; @@ -34,8 +35,8 @@ export interface HttpServiceSetup extends HttpServerSetup { } /** @public */ export interface HttpServiceStart { - /** Indicates if http server is listening on a port */ - isListening: () => boolean; + /** Indicates if http server is listening on a given port */ + isListening: (port: number) => boolean; } /** @internal */ @@ -48,6 +49,7 @@ export class HttpService implements CoreService { + isListening: (port: number = 0) => { const server = this.secondaryServers.get(port); if (server) return server.isListening(); return this.httpServer.isListening(); @@ -110,6 +117,18 @@ export class HttpService implements CoreService) { const { port } = cfg; const config = await this.config$.pipe(first()).toPromise(); @@ -145,9 +164,37 @@ export class HttpService implements CoreService s.stop())); this.secondaryServers.clear(); } + + private async runNotReadyServer(config: HttpConfig) { + this.log.debug('starting NotReady server'); + const httpServer = new HttpServer(this.log); + const { server } = await httpServer.setup(config); + this.notReadyServer = server; + // use hapi server while Kibana ResponseFactory doesn't allow specifying custom headers + // https://github.com/elastic/kibana/issues/33779 + this.notReadyServer.route({ + path: '/{p*}', + method: '*', + handler: (req, responseToolkit) => { + this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url}.`); + + // If server is not ready yet, because plugins or core can perform + // long running tasks (build assets, saved objects migrations etc.) + // we should let client know that and ask to retry after 30 seconds. + return responseToolkit + .response('Kibana server is not ready yet') + .code(503) + .header('Retry-After', '30'); + }, + }); + await this.notReadyServer.start(); + } } diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 056ee53cee89b..e9c2425bc82cf 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -19,7 +19,9 @@ export { config, HttpConfig, HttpConfigType } from './http_config'; export { HttpService, HttpServiceSetup, HttpServiceStart } from './http_service'; +export { GetAuthHeaders } from './auth_headers_storage'; export { + isRealRequest, KibanaRequest, KibanaRequestRoute, Router, @@ -28,6 +30,6 @@ export { } from './router'; export { BasePathProxyServer } from './base_path_proxy_server'; export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; -export { AuthenticationHandler, AuthToolkit } from './lifecycle/auth'; +export { AuthenticationHandler, AuthHeaders, AuthResultData, AuthToolkit } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; export { SessionStorageFactory, SessionStorage } from './session_storage'; diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/foo/src/index.js b/src/core/server/http/integration_tests/http_service.test.mocks.ts similarity index 83% rename from packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/foo/src/index.js rename to src/core/server/http/integration_tests/http_service.test.mocks.ts index 5611f96529819..3982df567ed7c 100644 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/foo/src/index.js +++ b/src/core/server/http/integration_tests/http_service.test.mocks.ts @@ -17,8 +17,7 @@ * under the License. */ -import bar from '@elastic/bar'; // eslint-disable-line import/no-unresolved - -export default function(val) { - return 'test [' + val + '] (' + bar(val) + ')'; -} +export const clusterClientMock = jest.fn(); +jest.doMock('../../elasticsearch/scoped_cluster_client', () => ({ + ScopedClusterClient: clusterClientMock, +})); diff --git a/src/core/server/http/integration_tests/http_service.test.ts b/src/core/server/http/integration_tests/http_service.test.ts index 21207d0b90e25..d1345b0884438 100644 --- a/src/core/server/http/integration_tests/http_service.test.ts +++ b/src/core/server/http/integration_tests/http_service.test.ts @@ -17,6 +17,9 @@ * under the License. */ import Boom from 'boom'; +import { Request } from 'hapi'; +import { first } from 'rxjs/operators'; +import { clusterClientMock } from './http_service.test.mocks'; import { Router } from '../router'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; @@ -48,16 +51,20 @@ describe('http service', () => { root = kbnTestServer.createRoot(); }, 30000); - afterEach(async () => await root.shutdown()); + afterEach(async () => { + clusterClientMock.mockClear(); + await root.shutdown(); + }); - it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => { + it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => { const { http } = await root.setup(); const { sessionStorageFactory } = await http.registerAuth((req, t) => { - if (req.headers.authorization) { + const headers = req.getFilteredHeaders(['authorization']); + if (headers.authorization) { const user = { id: '42' }; const sessionStorage = sessionStorageFactory.asScoped(req); sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated(user); + return t.authenticated({ state: user }); } else { return t.rejected(Boom.unauthorized()); } @@ -76,18 +83,57 @@ describe('http service', () => { .get(root, legacyUrl) .expect(200, 'ok from legacy server'); - expect(response.header['set-cookie']).toBe(undefined); + expect(response.header['set-cookie']).toHaveLength(1); }); - it('Should pass associated auth state to Legacy platform', async () => { + it('passes authHeaders as request headers to the legacy platform', async () => { + const token = 'Basic: name:password'; + const { http } = await root.setup(); + const { sessionStorageFactory } = await http.registerAuth((req, t) => { + const headers = req.getFilteredHeaders(['authorization']); + if (headers.authorization) { + const user = { id: '42' }; + const sessionStorage = sessionStorageFactory.asScoped(req); + sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); + return t.authenticated({ + state: user, + headers: { + authorization: token, + }, + }); + } else { + return t.rejected(Boom.unauthorized()); + } + }, cookieOptions); + await root.start(); + + const legacyUrl = '/legacy'; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyUrl, + handler: (req: Request) => ({ + authorization: req.headers.authorization, + custom: req.headers.custom, + }), + }); + + await kbnTestServer.request + .get(root, legacyUrl) + .set({ custom: 'custom-header' }) + .expect(200, { authorization: token, custom: 'custom-header' }); + }); + + it('passes associated auth state to Legacy platform', async () => { const user = { id: '42' }; const { http } = await root.setup(); const { sessionStorageFactory } = await http.registerAuth((req, t) => { - if (req.headers.authorization) { + const headers = req.getFilteredHeaders(['authorization']); + if (headers.authorization) { const sessionStorage = sessionStorageFactory.asScoped(req); sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs }); - return t.authenticated(user); + return t.authenticated({ state: user }); } else { return t.rejected(Boom.unauthorized()); } @@ -106,7 +152,61 @@ describe('http service', () => { expect(response.body.state).toEqual(user); expect(response.body.status).toEqual('authenticated'); - expect(response.header['set-cookie']).toBe(undefined); + expect(response.header['set-cookie']).toHaveLength(1); + }); + + it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => { + const authHeaders = { authorization: 'Basic: user:password' }; + const { http, elasticsearch } = await root.setup(); + const { registerAuth, registerRouter } = http; + + await registerAuth((req, t) => { + return t.authenticated({ headers: authHeaders }); + }, cookieOptions); + + const router = new Router('/new-platform'); + router.get({ path: '/', validate: false }, async (req, res) => { + const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); + client.asScoped(req); + return res.ok({ header: 'ok' }); + }); + registerRouter(router); + + await root.start(); + + await kbnTestServer.request.get(root, '/new-platform/').expect(200); + expect(clusterClientMock).toBeCalledTimes(1); + const [firstCall] = clusterClientMock.mock.calls; + const [, , headers] = firstCall; + expect(headers).toEqual(authHeaders); + }); + + it('pass request authorization header to Elasticsearch if registerAuth was not set', async () => { + const authorizationHeader = 'Basic: username:password'; + const { http, elasticsearch } = await root.setup(); + const { registerRouter } = http; + + const router = new Router('/new-platform'); + router.get({ path: '/', validate: false }, async (req, res) => { + const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); + client.asScoped(req); + return res.ok({ header: 'ok' }); + }); + registerRouter(router); + + await root.start(); + + await kbnTestServer.request + .get(root, '/new-platform/') + .set('Authorization', authorizationHeader) + .expect(200); + + expect(clusterClientMock).toBeCalledTimes(1); + const [firstCall] = clusterClientMock.mock.calls; + const [, , headers] = firstCall; + expect(headers).toEqual({ + authorization: authorizationHeader, + }); }); }); @@ -117,8 +217,8 @@ describe('http service', () => { }, 30000); afterEach(async () => await root.shutdown()); - it('Should support passing request through to the route handler', async () => { - const router = new Router(''); + it('supports passing request through to the route handler', async () => { + const router = new Router('/new-platform'); router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); const { http } = await root.setup(); @@ -130,20 +230,20 @@ describe('http service', () => { http.registerRouter(router); await root.start(); - await kbnTestServer.request.get(root, '/').expect(200, { content: 'ok' }); + await kbnTestServer.request.get(root, '/new-platform/').expect(200, { content: 'ok' }); }); - it('Should support redirecting to configured url', async () => { + it('supports redirecting to configured url', async () => { const redirectTo = '/redirect-url'; const { http } = await root.setup(); http.registerOnPostAuth(async (req, t) => t.redirected(redirectTo)); await root.start(); - const response = await kbnTestServer.request.get(root, '/').expect(302); + const response = await kbnTestServer.request.get(root, '/new-platform/').expect(302); expect(response.header.location).toBe(redirectTo); }); - it('Should failing a request with configured error and status code', async () => { + it('fails a request with configured error and status code', async () => { const { http } = await root.setup(); http.registerOnPostAuth(async (req, t) => t.rejected(new Error('unexpected error'), { statusCode: 400 }) @@ -151,25 +251,25 @@ describe('http service', () => { await root.start(); await kbnTestServer.request - .get(root, '/') + .get(root, '/new-platform/') .expect(400, { statusCode: 400, error: 'Bad Request', message: 'unexpected error' }); }); - it(`Shouldn't expose internal error details`, async () => { + it(`doesn't expose internal error details`, async () => { const { http } = await root.setup(); http.registerOnPostAuth(async (req, t) => { throw new Error('sensitive info'); }); await root.start(); - await kbnTestServer.request.get(root, '/').expect({ + await kbnTestServer.request.get(root, '/new-platform/').expect({ statusCode: 500, error: 'Internal Server Error', message: 'An internal server error occurred', }); }); - it(`Shouldn't share request object between interceptors`, async () => { + it(`doesn't share request object between interceptors`, async () => { const { http } = await root.setup(); http.registerOnPostAuth(async (req, t) => { // @ts-ignore. don't complain customField is not defined on Request type @@ -183,7 +283,7 @@ describe('http service', () => { } return t.next(); }); - const router = new Router(''); + const router = new Router('/new-platform'); router.get({ path: '/', validate: false }, async (req, res) => // @ts-ignore. don't complain customField is not defined on Request type res.ok({ customField: String(req.customField) }) @@ -191,7 +291,9 @@ describe('http service', () => { http.registerRouter(router); await root.start(); - await kbnTestServer.request.get(root, '/').expect(200, { customField: 'undefined' }); + await kbnTestServer.request + .get(root, '/new-platform/') + .expect(200, { customField: 'undefined' }); }); }); @@ -205,10 +307,10 @@ describe('http service', () => { it('supports Url change on the flight', async () => { const { http } = await root.setup(); http.registerOnPreAuth((req, t) => { - return t.redirected('/new-url', { forward: true }); + return t.redirected('/new-platform/new-url', { forward: true }); }); - const router = new Router('/'); + const router = new Router('/new-platform'); router.get({ path: '/new-url', validate: false }, async (req, res) => res.ok({ key: 'new-url-reached' }) ); diff --git a/src/core/server/http/lifecycle/auth.test.ts b/src/core/server/http/lifecycle/auth.test.ts index 031556c70483c..668d2a4fd11dc 100644 --- a/src/core/server/http/lifecycle/auth.test.ts +++ b/src/core/server/http/lifecycle/auth.test.ts @@ -22,10 +22,14 @@ import { adoptToHapiAuthFormat } from './auth'; import { httpServerMock } from '../http_server.mocks'; describe('adoptToHapiAuthFormat', () => { - it('Should allow authenticating a user identity with given credentials', async () => { - const credentials = {}; + it('allows to associate arbitrary data with an incoming request', async () => { + const authData = { + state: { foo: 'bar' }, + headers: { authorization: 'baz' }, + }; const authenticatedMock = jest.fn(); - const onAuth = adoptToHapiAuthFormat((req, t) => t.authenticated(credentials)); + const onSuccessMock = jest.fn(); + const onAuth = adoptToHapiAuthFormat((req, t) => t.authenticated(authData), onSuccessMock); await onAuth( httpServerMock.createRawRequest(), httpServerMock.createRawResponseToolkit({ @@ -34,12 +38,17 @@ describe('adoptToHapiAuthFormat', () => { ); expect(authenticatedMock).toBeCalledTimes(1); - expect(authenticatedMock).toBeCalledWith({ credentials }); + expect(authenticatedMock).toBeCalledWith({ credentials: authData.state }); + + expect(onSuccessMock).toBeCalledTimes(1); + const [[, onSuccessData]] = onSuccessMock.mock.calls; + expect(onSuccessData).toEqual(authData); }); it('Should allow redirecting to specified url', async () => { const redirectUrl = '/docs'; - const onAuth = adoptToHapiAuthFormat((req, t) => t.redirected(redirectUrl)); + const onSuccessMock = jest.fn(); + const onAuth = adoptToHapiAuthFormat((req, t) => t.redirected(redirectUrl), onSuccessMock); const takeoverSymbol = {}; const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol })); const result = await onAuth( @@ -51,11 +60,14 @@ describe('adoptToHapiAuthFormat', () => { expect(redirectMock).toBeCalledWith(redirectUrl); expect(result).toBe(takeoverSymbol); + expect(onSuccessMock).not.toHaveBeenCalled(); }); it('Should allow to specify statusCode and message for Boom error', async () => { - const onAuth = adoptToHapiAuthFormat((req, t) => - t.rejected(new Error('not found'), { statusCode: 404 }) + const onSuccessMock = jest.fn(); + const onAuth = adoptToHapiAuthFormat( + (req, t) => t.rejected(new Error('not found'), { statusCode: 404 }), + onSuccessMock ); const result = (await onAuth( httpServerMock.createRawRequest(), @@ -65,6 +77,7 @@ describe('adoptToHapiAuthFormat', () => { expect(result).toBeInstanceOf(Boom); expect(result.message).toBe('not found'); expect(result.output.statusCode).toBe(404); + expect(onSuccessMock).not.toHaveBeenCalled(); }); it('Should return Boom.internal error error if interceptor throws', async () => { diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index bcb7e454b4119..b866c00a756cc 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -19,6 +19,7 @@ import Boom from 'boom'; import { noop } from 'lodash'; import { Lifecycle, Request, ResponseToolkit } from 'hapi'; +import { KibanaRequest } from '../router'; enum ResultType { authenticated = 'authenticated', @@ -26,9 +27,8 @@ enum ResultType { rejected = 'rejected', } -interface Authenticated { +interface Authenticated extends AuthResultData { type: ResultType.authenticated; - state: object; } interface Redirected { @@ -45,8 +45,12 @@ interface Rejected { type AuthResult = Authenticated | Rejected | Redirected; const authResult = { - authenticated(state: object = {}): AuthResult { - return { type: ResultType.authenticated, state }; + authenticated(data: Partial = {}): AuthResult { + return { + type: ResultType.authenticated, + state: data.state || {}, + headers: data.headers || {}, + }; }, redirected(url: string): AuthResult { return { type: ResultType.redirected, url }; @@ -73,13 +77,35 @@ const authResult = { }, }; +/** + * Auth Headers map + * @public + * */ + +export type AuthHeaders = Record; + +/** + * Result of an incoming request authentication. + * @public + * */ +export interface AuthResultData { + /** + * Data to associate with an incoming request. Any downstream plugin may get access to the data. + */ + state: Record; + /** + * Auth specific headers to authenticate a user against Elasticsearch. + */ + headers: AuthHeaders; +} + /** * @public * A tool set defining an outcome of Auth interceptor for incoming request. */ export interface AuthToolkit { /** Authentication is successful with given credentials, allow request to pass through */ - authenticated: (state?: object) => AuthResult; + authenticated: (data?: Partial) => AuthResult; /** Authentication requires to interrupt request handling and redirect to a configured url */ redirected: (url: string) => AuthResult; /** Authentication is unsuccessful, fail the request with specified error. */ @@ -94,29 +120,29 @@ const toolkit: AuthToolkit = { /** @public */ export type AuthenticationHandler = ( - request: Readonly, + request: KibanaRequest, t: AuthToolkit ) => AuthResult | Promise; /** @public */ export function adoptToHapiAuthFormat( fn: AuthenticationHandler, - onSuccess: (req: Request, state: unknown) => void = noop + onSuccess: (req: Request, data: AuthResultData) => void = noop ) { return async function interceptAuth( req: Request, h: ResponseToolkit ): Promise { try { - const result = await fn(req, toolkit); + const result = await fn(KibanaRequest.from(req, undefined, false), toolkit); if (!authResult.isValid(result)) { throw new Error( `Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.` ); } if (authResult.isAuthenticated(result)) { - onSuccess(req, result.state); - return h.authenticated({ credentials: result.state }); + onSuccess(req, { state: result.state, headers: result.headers }); + return h.authenticated({ credentials: result.state || {} }); } if (authResult.isRedirected(result)) { return h.redirect(result.url).takeover(); diff --git a/src/core/server/http/router/headers.ts b/src/core/server/http/router/headers.ts index d578542d7a9ce..956ef8cd2ebb8 100644 --- a/src/core/server/http/router/headers.ts +++ b/src/core/server/http/router/headers.ts @@ -24,9 +24,16 @@ export type Headers = Record; const normalizeHeaderField = (field: string) => field.trim().toLowerCase(); -export function filterHeaders(headers: Headers, fieldsToKeep: string[]) { +export function filterHeaders( + headers: Headers, + fieldsToKeep: string[], + fieldsToExclude: string[] = [] +) { + const fieldsToExcludeNormalized = fieldsToExclude.map(normalizeHeaderField); // Normalize list of headers we want to allow in upstream request - const fieldsToKeepNormalized = fieldsToKeep.map(normalizeHeaderField); + const fieldsToKeepNormalized = fieldsToKeep + .map(normalizeHeaderField) + .filter(name => !fieldsToExcludeNormalized.includes(name)); return pick(headers, fieldsToKeepNormalized); } diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index cb941326e23f1..ad088756ddd2e 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -19,5 +19,11 @@ export { Headers, filterHeaders } from './headers'; export { Router } from './router'; -export { KibanaRequest, KibanaRequestRoute, toRawRequest } from './request'; +export { + KibanaRequest, + KibanaRequestRoute, + ensureRawRequest, + isRealRequest, + getIncomingMessage, +} from './request'; export { RouteMethod, RouteConfigOptions } from './route'; diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts new file mode 100644 index 0000000000000..6aad68e48638e --- /dev/null +++ b/src/core/server/http/router/request.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { KibanaRequest } from './request'; +import { httpServerMock } from '../http_server.mocks'; + +describe('KibanaRequest', () => { + describe('get all headers', () => { + it('returns all headers', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one', authorization: 'token' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.headers).toEqual({ custom: 'one', authorization: 'token' }); + }); + }); + + describe('#getFilteredHeaders', () => { + it('returns request headers', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.getFilteredHeaders(['custom'])).toEqual({ + custom: 'one', + }); + }); + + it('normalizes a header name', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.getFilteredHeaders(['CUSTOM'])).toEqual({ + custom: 'one', + }); + }); + + it('returns an empty object is no headers were specified', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.getFilteredHeaders([])).toEqual({}); + }); + + it("doesn't expose authorization header by default", () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one', authorization: 'token' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.getFilteredHeaders(['custom', 'authorization'])).toEqual({ + custom: 'one', + }); + }); + + it('exposes authorization header if secured = false', () => { + const request = httpServerMock.createRawRequest({ + headers: { custom: 'one', authorization: 'token' }, + }); + const kibanaRequest = KibanaRequest.from(request, undefined, false); + expect(kibanaRequest.getFilteredHeaders(['custom', 'authorization'])).toEqual({ + custom: 'one', + authorization: 'token', + }); + }); + }); +}); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 3c235ffbf8bd9..c81eda208a0df 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -18,6 +18,7 @@ */ import { Url } from 'url'; +import { IncomingMessage } from 'http'; import { ObjectType, TypeOf } from '@kbn/config-schema'; import { Request } from 'hapi'; @@ -37,8 +38,15 @@ export interface KibanaRequestRoute { options: Required; } +const secretHeaders = ['authorization']; /** * Kibana specific abstraction for an incoming request. + * + * @remarks + * The `headers` property will be deprecated and removed in future versions + * of this class. Please use the `getFilteredHeaders` method to acesss the + * list of headers available + * * @public * */ export class KibanaRequest { @@ -46,13 +54,21 @@ export class KibanaRequest { * Factory for creating requests. Validates the request before creating an * instance of a KibanaRequest. * @internal + * */ public static from

( req: Request, - routeSchemas?: RouteSchemas + routeSchemas?: RouteSchemas, + withoutSecretHeaders: boolean = true ) { const requestParts = KibanaRequest.validate(req, routeSchemas); - return new KibanaRequest(req, requestParts.params, requestParts.query, requestParts.body); + return new KibanaRequest( + req, + requestParts.params, + requestParts.query, + requestParts.body, + withoutSecretHeaders + ); } /** @@ -86,9 +102,13 @@ export class KibanaRequest { return { query, params, body }; } - public readonly headers: Headers; public readonly url: Url; public readonly route: RecursiveReadonly; + /** + * This property will be removed in future version of this class, please + * use the `getFilteredHeaders` method instead + */ + public readonly headers: Headers; /** @internal */ protected readonly [requestSymbol]: Request; @@ -97,17 +117,27 @@ export class KibanaRequest { request: Request, readonly params: Params, readonly query: Query, - readonly body: Body + readonly body: Body, + private readonly withoutSecretHeaders: boolean ) { - this.headers = request.headers; this.url = request.url; + this.headers = request.headers; + + // prevent Symbol exposure via Object.getOwnPropertySymbols() + Object.defineProperty(this, requestSymbol, { + value: request, + enumerable: false, + }); - this[requestSymbol] = request; this.route = deepFreeze(this.getRouteInfo()); } public getFilteredHeaders(headersToKeep: string[]) { - return filterHeaders(this.headers, headersToKeep); + return filterHeaders( + this[requestSymbol].headers, + headersToKeep, + this.withoutSecretHeaders ? secretHeaders : [] + ); } private getRouteInfo() { @@ -124,7 +154,38 @@ export class KibanaRequest { } /** - * Returns underlying Hapi Request object for KibanaRequest + * Returns underlying Hapi Request * @internal */ -export const toRawRequest = (request: KibanaRequest) => request[requestSymbol]; +export const ensureRawRequest = (request: KibanaRequest | Request) => + isKibanaRequest(request) ? request[requestSymbol] : request; + +/** + * Returns http.IncomingMessage that is used an identifier for New Platform KibanaRequest + * and Legacy platform Hapi Request. + * Exposed while New platform supports Legacy Platform. + * @internal + */ +export const getIncomingMessage = (request: KibanaRequest | Request): IncomingMessage => { + return ensureRawRequest(request).raw.req; +}; + +function isKibanaRequest(request: unknown): request is KibanaRequest { + return request instanceof KibanaRequest; +} + +function isRequest(request: any): request is Request { + try { + return request.raw.req && typeof request.raw.req === 'object'; + } catch { + return false; + } +} + +/** + * Checks if an incoming request either KibanaRequest or Legacy.Request + * @internal + */ +export function isRealRequest(request: unknown): request is KibanaRequest | Request { + return isKibanaRequest(request) || isRequest(request); +} diff --git a/src/core/server/http/session_storage.ts b/src/core/server/http/session_storage.ts index 2c726ce34a3cb..3e2b51f1848b1 100644 --- a/src/core/server/http/session_storage.ts +++ b/src/core/server/http/session_storage.ts @@ -17,7 +17,6 @@ * under the License. */ -import { Request } from 'hapi'; import { KibanaRequest } from './router'; /** * Provides an interface to store and retrieve data across requests. @@ -43,5 +42,5 @@ export interface SessionStorage { * SessionStorage factory to bind one to an incoming request * @public */ export interface SessionStorageFactory { - asScoped: (request: Readonly | KibanaRequest) => SessionStorage; + asScoped: (request: KibanaRequest) => SessionStorage; } diff --git a/src/core/server/index.test.mocks.ts b/src/core/server/index.test.mocks.ts index 4e61316fcff94..9526a7d79ee43 100644 --- a/src/core/server/index.test.mocks.ts +++ b/src/core/server/index.test.mocks.ts @@ -18,9 +18,9 @@ */ import { httpServiceMock } from './http/http_service.mock'; -export const httpService = httpServiceMock.create(); +export const mockHttpService = httpServiceMock.create(); jest.doMock('./http/http_service', () => ({ - HttpService: jest.fn(() => httpService), + HttpService: jest.fn(() => mockHttpService), })); import { pluginServiceMock } from './plugins/plugins_service.mock'; @@ -30,9 +30,9 @@ jest.doMock('./plugins/plugins_service', () => ({ })); import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; -export const elasticsearchService = elasticsearchServiceMock.create(); +export const mockElasticsearchService = elasticsearchServiceMock.create(); jest.doMock('./elasticsearch/elasticsearch_service', () => ({ - ElasticsearchService: jest.fn(() => elasticsearchService), + ElasticsearchService: jest.fn(() => mockElasticsearchService), })); export const mockLegacyService = { setup: jest.fn(), start: jest.fn(), stop: jest.fn() }; @@ -41,7 +41,7 @@ jest.mock('./legacy/legacy_service', () => ({ })); import { configServiceMock } from './config/config_service.mock'; -export const configService = configServiceMock.create(); +export const mockConfigService = configServiceMock.create(); jest.doMock('./config/config_service', () => ({ - ConfigService: jest.fn(() => configService), + ConfigService: jest.fn(() => mockConfigService), })); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index f901b25e1ae87..51727b6e02cf1 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -49,10 +49,15 @@ export { ScopedClusterClient, ElasticsearchClientConfig, APICaller, + FakeRequest, + LegacyRequest, } from './elasticsearch'; export { AuthenticationHandler, + AuthHeaders, + AuthResultData, AuthToolkit, + GetAuthHeaders, KibanaRequest, KibanaRequestRoute, OnPreAuthHandler, @@ -75,6 +80,28 @@ export { PluginName, } from './plugins'; +export { + SavedObject, + SavedObjectAttributes, + SavedObjectReference, + SavedObjectsBaseOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkResponse, + SavedObjectsClient, + SavedObjectsClientContract, + SavedObjectsCreateOptions, + SavedObjectsClientWrapperFactory, + SavedObjectsClientWrapperOptions, + SavedObjectsErrorHelpers, + SavedObjectsFindOptions, + SavedObjectsFindResponse, + SavedObjectsMigrationVersion, + SavedObjectsService, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, +} from './saved_objects'; + export { RecursiveReadonly } from '../utils'; /** @@ -114,7 +141,6 @@ export interface InternalCoreSetup { * @public */ export interface InternalCoreStart { - http: HttpServiceStart; plugins: PluginsServiceStart; } diff --git a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap index bc7e8f72c4d61..5319f093a4b8f 100644 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap @@ -46,27 +46,6 @@ Array [ exports[`once LegacyService is set up with connection info creates legacy kbnServer and closes it if \`listen\` fails. 1`] = `"something failed"`; -exports[`once LegacyService is set up with connection info proxy route responds with \`503\` if \`kbnServer\` is not ready yet.: 503 response 1`] = ` -Object { - "body": Array [ - Array [ - "Kibana server is not ready yet", - ], - ], - "code": Array [ - Array [ - 503, - ], - ], - "header": Array [ - Array [ - "Retry-After", - "30", - ], - ], -} -`; - exports[`once LegacyService is set up with connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` Array [ Array [ @@ -79,26 +58,6 @@ Array [ ] `; -exports[`once LegacyService is set up with connection info register proxy route.: proxy route options 1`] = ` -Array [ - Array [ - Object { - "handler": [Function], - "method": "*", - "options": Object { - "payload": Object { - "maxBytes": 9007199254740991, - "output": "stream", - "parse": false, - "timeout": false, - }, - }, - "path": "/{p*}", - }, - ], -] -`; - exports[`once LegacyService is set up with connection info throws if fails to retrieve initial config. 1`] = `"something failed"`; exports[`once LegacyService is set up without connection info reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts new file mode 100644 index 0000000000000..f4b2d27470087 --- /dev/null +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Router } from '../../http/'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +describe('legacy service', () => { + describe('http server', () => { + let root: ReturnType; + beforeEach(() => { + root = kbnTestServer.createRoot(); + }, 30000); + + afterEach(async () => await root.shutdown()); + + it("handles http request in Legacy platform if New platform doesn't handle it", async () => { + const rootUrl = '/route'; + const router = new Router(rootUrl); + router.get({ path: '/new-platform', validate: false }, (req, res) => + res.ok({ content: 'from-new-platform' }) + ); + + const { http } = await root.setup(); + http.registerRouter(router); + await root.start(); + + const legacyPlatformUrl = `${rootUrl}/legacy-platform`; + const kbnServer = kbnTestServer.getKbnServer(root); + kbnServer.server.route({ + method: 'GET', + path: legacyPlatformUrl, + handler: () => 'ok from legacy server', + }); + + await kbnTestServer.request + .get(root, '/route/new-platform') + .expect(200, { content: 'from-new-platform' }); + + await kbnTestServer.request.get(root, legacyPlatformUrl).expect(200, 'ok from legacy server'); + }); + it('throws error if Legacy and New platforms register handler for the same route', async () => { + const rootUrl = '/route'; + const router = new Router(rootUrl); + router.get({ path: '', validate: false }, (req, res) => + res.ok({ content: 'from-new-platform' }) + ); + + const { http } = await root.setup(); + http.registerRouter(router); + await root.start(); + + const kbnServer = kbnTestServer.getKbnServer(root); + expect(() => + kbnServer.server.route({ + method: 'GET', + path: rootUrl, + handler: () => 'ok from legacy server', + }) + ).toThrowErrorMatchingInlineSnapshot(`"New route /route conflicts with existing /route"`); + }); + }); +}); diff --git a/src/core/server/legacy/legacy_platform_proxy.test.ts b/src/core/server/legacy/legacy_platform_proxy.test.ts deleted file mode 100644 index 29c91bd0b61f9..0000000000000 --- a/src/core/server/legacy/legacy_platform_proxy.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Server } from 'net'; - -import { LegacyPlatformProxy } from './legacy_platform_proxy'; - -let server: jest.Mocked; -let proxy: LegacyPlatformProxy; -beforeEach(() => { - server = { - addListener: jest.fn(), - address: jest - .fn() - .mockReturnValue({ port: 1234, family: 'test-family', address: 'test-address' }), - getConnections: jest.fn(), - } as any; - proxy = new LegacyPlatformProxy({ debug: jest.fn() } as any, server); -}); - -test('correctly redirects server events.', () => { - for (const eventName of ['clientError', 'close', 'connection', 'error', 'listening', 'upgrade']) { - expect(server.addListener).toHaveBeenCalledWith(eventName, expect.any(Function)); - - const listener = jest.fn(); - proxy.addListener(eventName, listener); - - // Emit several events, to make sure that server is not being listened with `once`. - const [, serverListener] = server.addListener.mock.calls.find( - ([serverEventName]) => serverEventName === eventName - )!; - - (serverListener as jest.Mock)(1, 2, 3, 4); - (serverListener as jest.Mock)(5, 6, 7, 8); - - expect(listener).toHaveBeenCalledTimes(2); - expect(listener).toHaveBeenCalledWith(1, 2, 3, 4); - - proxy.removeListener(eventName, listener); - } -}); - -test('returns `address` from the underlying server.', () => { - expect(proxy.address()).toEqual({ - address: 'test-address', - family: 'test-family', - port: 1234, - }); -}); - -test('`listen` calls callback immediately.', async () => { - const onListenComplete = jest.fn(); - - await proxy.listen(1234, 'host-1', onListenComplete); - - expect(onListenComplete).toHaveBeenCalledTimes(1); -}); - -test('`close` calls callback immediately.', async () => { - const onCloseComplete = jest.fn(); - - await proxy.close(onCloseComplete); - - expect(onCloseComplete).toHaveBeenCalledTimes(1); -}); - -test('returns connection count from the underlying server.', () => { - server.getConnections.mockImplementation(callback => callback(null, 0)); - const onGetConnectionsComplete = jest.fn(); - proxy.getConnections(onGetConnectionsComplete); - - expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); - expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 0); - onGetConnectionsComplete.mockReset(); - - server.getConnections.mockImplementation(callback => callback(null, 100500)); - proxy.getConnections(onGetConnectionsComplete); - - expect(onGetConnectionsComplete).toHaveBeenCalledTimes(1); - expect(onGetConnectionsComplete).toHaveBeenCalledWith(null, 100500); -}); diff --git a/src/core/server/legacy/legacy_platform_proxy.ts b/src/core/server/legacy/legacy_platform_proxy.ts deleted file mode 100644 index a78787d87c055..0000000000000 --- a/src/core/server/legacy/legacy_platform_proxy.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EventEmitter } from 'events'; -import { Server } from 'net'; - -import { Logger } from '../logging'; - -/** - * List of the server events to be forwarded to the legacy platform. - */ -const ServerEventsToForward = [ - 'clientError', - 'close', - 'connection', - 'error', - 'listening', - 'upgrade', -]; - -/** - * Represents "proxy" between legacy and current platform. - * @internal - */ -export class LegacyPlatformProxy extends EventEmitter { - private readonly eventHandlers: Map void>; - - constructor(private readonly log: Logger, private readonly server: Server) { - super(); - - // HapiJS expects that the following events will be generated by `listener`, see: - // https://github.com/hapijs/hapi/blob/v14.2.0/lib/connection.js. - this.eventHandlers = new Map( - ServerEventsToForward.map(eventName => { - return [ - eventName, - (...args: any[]) => { - this.log.debug(`Event is being forwarded: ${eventName}`); - this.emit(eventName, ...args); - }, - ] as [string, (...args: any[]) => void]; - }) - ); - - for (const [eventName, eventHandler] of this.eventHandlers) { - this.server.addListener(eventName, eventHandler); - } - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public address() { - this.log.debug('"address" has been called.'); - - return this.server.address(); - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public listen(port: number, host: string, callback?: (error?: Error) => void) { - this.log.debug(`"listen" has been called (${host}:${port}).`); - - if (callback !== undefined) { - callback(); - } - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public close(callback?: (error?: Error) => void) { - this.log.debug('"close" has been called.'); - - if (callback !== undefined) { - callback(); - } - } - - /** - * Neither new nor legacy platform should use this method directly. - */ - public getConnections(callback: (error: Error | null, count?: number) => void) { - // This method is used by `even-better` (before we start platform). - // It seems that the latest version of parent `good` doesn't use this anymore. - this.server.getConnections(callback); - } -} diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 759a2eb76fd0c..fa4d60520818e 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -17,13 +17,11 @@ * under the License. */ -import { BehaviorSubject, Subject, throwError } from 'rxjs'; +import { BehaviorSubject, throwError } from 'rxjs'; -jest.mock('./legacy_platform_proxy'); jest.mock('../../../legacy/server/kbn_server'); jest.mock('../../../cli/cluster/cluster_manager'); -import { first } from 'rxjs/operators'; import { LegacyService } from '.'; // @ts-ignore: implicit any for JS file import MockClusterManager from '../../../cli/cluster/cluster_manager'; @@ -36,10 +34,8 @@ import { HttpServiceStart, BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_service'; -import { LegacyPlatformProxy } from './legacy_platform_proxy'; const MockKbnServer: jest.Mock = KbnServer as any; -const MockLegacyPlatformProxy: jest.Mock = LegacyPlatformProxy as any; let env: Env; let config$: BehaviorSubject; @@ -73,8 +69,9 @@ beforeEach(() => { core: { elasticsearch: { legacy: {} } as any, http: { - options: { someOption: 'foo', someAnotherOption: 'bar' }, - server: { listener: { addListener: jest.fn() }, route: jest.fn() }, + auth: { + getAuthHeaders: () => undefined, + }, }, plugins: { contracts: new Map([['plugin-id', 'plugin-value']]), @@ -113,72 +110,6 @@ afterEach(() => { }); describe('once LegacyService is set up with connection info', () => { - test('register proxy route.', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - expect(setupDeps.core.http.server.route.mock.calls).toMatchSnapshot('proxy route options'); - }); - - test('proxy route responds with `503` if `kbnServer` is not ready yet.', async () => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); - const legacyService = new LegacyService({ env, logger, configService: configService as any }); - - const kbnServerListen$ = new Subject(); - MockKbnServer.prototype.listen = jest.fn(() => { - kbnServerListen$.next(); - return kbnServerListen$.toPromise(); - }); - - // Wait until listen is called and proxy route is registered, but don't allow - // listen to complete and make kbnServer available. - await legacyService.setup(setupDeps); - const legacySetupPromise = legacyService.start(startDeps); - await kbnServerListen$.pipe(first()).toPromise(); - - const mockResponse: any = { - code: jest.fn().mockImplementation(() => mockResponse), - header: jest.fn().mockImplementation(() => mockResponse), - }; - const mockResponseToolkit = { - response: jest.fn().mockReturnValue(mockResponse), - abandon: Symbol('abandon'), - }; - const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; - - const [[{ handler }]] = setupDeps.core.http.server.route.mock.calls; - const response503 = await handler(mockRequest, mockResponseToolkit); - - expect(response503).toBe(mockResponse); - expect({ - body: mockResponseToolkit.response.mock.calls, - code: mockResponse.code.mock.calls, - header: mockResponse.header.mock.calls, - }).toMatchSnapshot('503 response'); - - // Make sure request hasn't been passed to the legacy platform. - const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances; - expect(mockedLegacyPlatformProxy.emit).not.toHaveBeenCalled(); - - // Now wait until kibana is ready and try to request once again. - kbnServerListen$.complete(); - await legacySetupPromise; - mockResponseToolkit.response.mockClear(); - - const responseProxy = await handler(mockRequest, mockResponseToolkit); - expect(responseProxy).toBe(mockResponseToolkit.abandon); - expect(mockResponseToolkit.response).not.toHaveBeenCalled(); - - // Make sure request has been passed to the legacy platform. - expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1); - expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith( - 'request', - mockRequest.raw.req, - mockRequest.raw.res - ); - }); - test('creates legacy kbnServer and calls `listen`.', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); const legacyService = new LegacyService({ env, logger, configService: configService as any }); @@ -192,11 +123,6 @@ describe('once LegacyService is set up with connection info', () => { { setupDeps, startDeps, - serverOptions: { - listener: expect.any(LegacyPlatformProxy), - someAnotherOption: 'bar', - someOption: 'foo', - }, handledConfigPaths: ['foo.bar'], logger, } @@ -220,11 +146,6 @@ describe('once LegacyService is set up with connection info', () => { { setupDeps, startDeps, - serverOptions: { - listener: expect.any(LegacyPlatformProxy), - someAnotherOption: 'bar', - someOption: 'foo', - }, handledConfigPaths: ['foo.bar'], logger, } @@ -309,59 +230,24 @@ describe('once LegacyService is set up with connection info', () => { expect(mockKbnServer.applyLoggingConfiguration).not.toHaveBeenCalled(); expect(loggingServiceMock.collect(logger).error).toEqual([[configError]]); }); - - test('proxy route abandons request processing and forwards it to the legacy Kibana', async () => { - const legacyService = new LegacyService({ env, logger, configService: configService as any }); - const mockResponseToolkit = { response: jest.fn(), abandon: Symbol('abandon') }; - const mockRequest = { raw: { req: { a: 1 }, res: { b: 2 } } }; - - await legacyService.setup(setupDeps); - await legacyService.start(startDeps); - - const [[{ handler }]] = setupDeps.core.http.server.route.mock.calls; - const response = await handler(mockRequest, mockResponseToolkit); - - expect(response).toBe(mockResponseToolkit.abandon); - expect(mockResponseToolkit.response).not.toHaveBeenCalled(); - - // Make sure request has been passed to the legacy platform. - const [mockedLegacyPlatformProxy] = MockLegacyPlatformProxy.mock.instances; - expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledTimes(1); - expect(mockedLegacyPlatformProxy.emit).toHaveBeenCalledWith( - 'request', - mockRequest.raw.req, - mockRequest.raw.res - ); - }); }); describe('once LegacyService is set up without connection info', () => { - const disabledHttpStartDeps = { - core: { - http: { - isListening: () => false, - }, - plugins: { contracts: new Map() }, - }, - plugins: {}, - }; let legacyService: LegacyService; beforeEach(async () => { legacyService = new LegacyService({ env, logger, configService: configService as any }); await legacyService.setup(setupDeps); - await legacyService.start(disabledHttpStartDeps); + await legacyService.start(startDeps); }); test('creates legacy kbnServer with `autoListen: false`.', () => { - expect(setupDeps.core.http.server.route).not.toHaveBeenCalled(); expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( { server: { autoListen: true } }, { setupDeps, - startDeps: disabledHttpStartDeps, - serverOptions: { autoListen: false }, + startDeps, handledConfigPaths: ['foo.bar'], logger, } @@ -402,15 +288,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { }); await devClusterLegacyService.setup(setupDeps); - await devClusterLegacyService.start({ - core: { - http: { - isListening: () => false, - }, - plugins: { contracts: new Map() }, - }, - plugins: {}, - }); + await devClusterLegacyService.start(startDeps); expect(MockClusterManager.create.mock.calls).toMatchSnapshot( 'cluster manager without base path proxy' @@ -430,15 +308,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { }); await devClusterLegacyService.setup(setupDeps); - await devClusterLegacyService.start({ - core: { - http: { - isListening: () => false, - }, - plugins: { contracts: new Map() }, - }, - plugins: {}, - }); + await devClusterLegacyService.start(startDeps); expect(MockClusterManager.create).toBeCalledTimes(1); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index fd1b46d7fa711..a50f049db2231 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -17,7 +17,6 @@ * under the License. */ -import { Server as HapiServer } from 'hapi'; import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators'; import { CoreService } from '../../types'; @@ -27,7 +26,6 @@ import { CoreContext } from '../core_context'; import { DevConfig, DevConfigType } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http'; import { Logger } from '../logging'; -import { LegacyPlatformProxy } from './legacy_platform_proxy'; interface LegacyKbnServer { applyLoggingConfiguration: (settings: Readonly>) => void; @@ -149,17 +147,6 @@ export class LegacyService implements CoreService { // eslint-disable-next-line @typescript-eslint/no-var-requires const KbnServer = require('../../../legacy/server/kbn_server'); const kbnServer: LegacyKbnServer = new KbnServer(getLegacyRawConfig(config), { - // If core HTTP service is run we'll receive internal server reference and - // options that were used to create that server so that we can properly - // bridge with the "legacy" Kibana. If server isn't run (e.g. if process is - // managed by ClusterManager or optimizer) then we won't have that info, - // so we can't start "legacy" server either. - serverOptions: startDeps.core.http.isListening() - ? { - ...setupDeps.core.http.options, - listener: this.setupProxyListener(setupDeps.core.http.server), - } - : { autoListen: false }, handledConfigPaths: await this.coreContext.configService.getUsedPaths(), setupDeps, startDeps, @@ -188,51 +175,4 @@ export class LegacyService implements CoreService { return kbnServer; } - - private setupProxyListener(server: HapiServer) { - const legacyProxy = new LegacyPlatformProxy( - this.coreContext.logger.get('legacy-proxy'), - server.listener - ); - - // We register Kibana proxy middleware right before we start server to allow - // all new platform plugins register their routes, so that `legacyProxy` - // handles only requests that aren't handled by the new platform. - server.route({ - path: '/{p*}', - method: '*', - options: { - payload: { - output: 'stream', - parse: false, - timeout: false, - // Having such a large value here will allow legacy routes to override - // maximum allowed payload size set in the core http server if needed. - maxBytes: Number.MAX_SAFE_INTEGER, - }, - }, - handler: async ({ raw: { req, res } }, responseToolkit) => { - if (this.kbnServer === undefined) { - this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url}.`); - - // If legacy server is not ready yet (e.g. it's still in optimization phase), - // we should let client know that and ask to retry after 30 seconds. - return responseToolkit - .response('Kibana server is not ready yet') - .code(503) - .header('Retry-After', '30'); - } - - this.log.trace(`Request will be handled by proxy ${req.method}:${req.url}.`); - - // Forward request and response objects to the legacy platform. This method - // is used whenever new platform doesn't know how to handle the request. - legacyProxy.emit('request', req, res); - - return responseToolkit.abandon; - }, - }); - - return legacyProxy; - } } diff --git a/src/legacy/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 similarity index 100% rename from src/legacy/server/saved_objects/export/get_sorted_objects_for_export.test.ts rename to src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts diff --git a/src/legacy/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts similarity index 100% rename from src/legacy/server/saved_objects/export/get_sorted_objects_for_export.ts rename to src/core/server/saved_objects/export/get_sorted_objects_for_export.ts diff --git a/src/legacy/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts similarity index 100% rename from src/legacy/server/saved_objects/export/index.ts rename to src/core/server/saved_objects/export/index.ts diff --git a/src/legacy/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts similarity index 100% rename from src/legacy/server/saved_objects/export/inject_nested_depdendencies.test.ts rename to src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts diff --git a/src/legacy/server/saved_objects/export/inject_nested_depdendencies.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts similarity index 100% rename from src/legacy/server/saved_objects/export/inject_nested_depdendencies.ts rename to src/core/server/saved_objects/export/inject_nested_depdendencies.ts diff --git a/src/legacy/server/saved_objects/export/sort_objects.test.ts b/src/core/server/saved_objects/export/sort_objects.test.ts similarity index 100% rename from src/legacy/server/saved_objects/export/sort_objects.test.ts rename to src/core/server/saved_objects/export/sort_objects.test.ts diff --git a/src/legacy/server/saved_objects/export/sort_objects.ts b/src/core/server/saved_objects/export/sort_objects.ts similarity index 100% rename from src/legacy/server/saved_objects/export/sort_objects.ts rename to src/core/server/saved_objects/export/sort_objects.ts diff --git a/src/legacy/server/saved_objects/import/collect_saved_objects.test.ts b/src/core/server/saved_objects/import/collect_saved_objects.test.ts similarity index 100% rename from src/legacy/server/saved_objects/import/collect_saved_objects.test.ts rename to src/core/server/saved_objects/import/collect_saved_objects.test.ts diff --git a/src/legacy/server/saved_objects/import/collect_saved_objects.ts b/src/core/server/saved_objects/import/collect_saved_objects.ts similarity index 98% rename from src/legacy/server/saved_objects/import/collect_saved_objects.ts rename to src/core/server/saved_objects/import/collect_saved_objects.ts index 95e32528149f8..3445fe9b42406 100644 --- a/src/legacy/server/saved_objects/import/collect_saved_objects.ts +++ b/src/core/server/saved_objects/import/collect_saved_objects.ts @@ -24,7 +24,7 @@ import { createMapStream, createPromiseFromStreams, createSplitStream, -} from '../../../utils/streams'; +} from '../../../../legacy/utils/streams'; import { SavedObject } from '../service'; import { createLimitStream } from './create_limit_stream'; import { ImportError } from './types'; diff --git a/src/legacy/server/saved_objects/import/create_limit_stream.test.ts b/src/core/server/saved_objects/import/create_limit_stream.test.ts similarity index 97% rename from src/legacy/server/saved_objects/import/create_limit_stream.test.ts rename to src/core/server/saved_objects/import/create_limit_stream.test.ts index ece1de2da752b..736cfadcb6222 100644 --- a/src/legacy/server/saved_objects/import/create_limit_stream.test.ts +++ b/src/core/server/saved_objects/import/create_limit_stream.test.ts @@ -21,7 +21,7 @@ import { createConcatStream, createListStream, createPromiseFromStreams, -} from '../../../utils/streams'; +} from '../../../../legacy/utils/streams'; import { createLimitStream } from './create_limit_stream'; describe('createLimitStream()', () => { diff --git a/src/legacy/server/saved_objects/import/create_limit_stream.ts b/src/core/server/saved_objects/import/create_limit_stream.ts similarity index 100% rename from src/legacy/server/saved_objects/import/create_limit_stream.ts rename to src/core/server/saved_objects/import/create_limit_stream.ts diff --git a/src/legacy/server/saved_objects/import/create_objects_filter.test.ts b/src/core/server/saved_objects/import/create_objects_filter.test.ts similarity index 100% rename from src/legacy/server/saved_objects/import/create_objects_filter.test.ts rename to src/core/server/saved_objects/import/create_objects_filter.test.ts diff --git a/src/legacy/server/saved_objects/import/create_objects_filter.ts b/src/core/server/saved_objects/import/create_objects_filter.ts similarity index 100% rename from src/legacy/server/saved_objects/import/create_objects_filter.ts rename to src/core/server/saved_objects/import/create_objects_filter.ts diff --git a/src/legacy/server/saved_objects/import/extract_errors.test.ts b/src/core/server/saved_objects/import/extract_errors.test.ts similarity index 100% rename from src/legacy/server/saved_objects/import/extract_errors.test.ts rename to src/core/server/saved_objects/import/extract_errors.test.ts diff --git a/src/legacy/server/saved_objects/import/extract_errors.ts b/src/core/server/saved_objects/import/extract_errors.ts similarity index 100% rename from src/legacy/server/saved_objects/import/extract_errors.ts rename to src/core/server/saved_objects/import/extract_errors.ts diff --git a/src/legacy/server/saved_objects/import/import_saved_objects.test.ts b/src/core/server/saved_objects/import/import_saved_objects.test.ts similarity index 100% rename from src/legacy/server/saved_objects/import/import_saved_objects.test.ts rename to src/core/server/saved_objects/import/import_saved_objects.test.ts diff --git a/src/legacy/server/saved_objects/import/import_saved_objects.ts b/src/core/server/saved_objects/import/import_saved_objects.ts similarity index 100% rename from src/legacy/server/saved_objects/import/import_saved_objects.ts rename to src/core/server/saved_objects/import/import_saved_objects.ts diff --git a/src/legacy/server/saved_objects/import/index.ts b/src/core/server/saved_objects/import/index.ts similarity index 100% rename from src/legacy/server/saved_objects/import/index.ts rename to src/core/server/saved_objects/import/index.ts diff --git a/src/legacy/server/saved_objects/import/resolve_import_errors.test.ts b/src/core/server/saved_objects/import/resolve_import_errors.test.ts similarity index 100% rename from src/legacy/server/saved_objects/import/resolve_import_errors.test.ts rename to src/core/server/saved_objects/import/resolve_import_errors.test.ts diff --git a/src/legacy/server/saved_objects/import/resolve_import_errors.ts b/src/core/server/saved_objects/import/resolve_import_errors.ts similarity index 100% rename from src/legacy/server/saved_objects/import/resolve_import_errors.ts rename to src/core/server/saved_objects/import/resolve_import_errors.ts diff --git a/src/legacy/server/saved_objects/import/split_overwrites.test.ts b/src/core/server/saved_objects/import/split_overwrites.test.ts similarity index 100% rename from src/legacy/server/saved_objects/import/split_overwrites.test.ts rename to src/core/server/saved_objects/import/split_overwrites.test.ts diff --git a/src/legacy/server/saved_objects/import/split_overwrites.ts b/src/core/server/saved_objects/import/split_overwrites.ts similarity index 100% rename from src/legacy/server/saved_objects/import/split_overwrites.ts rename to src/core/server/saved_objects/import/split_overwrites.ts diff --git a/src/legacy/server/saved_objects/import/types.ts b/src/core/server/saved_objects/import/types.ts similarity index 100% rename from src/legacy/server/saved_objects/import/types.ts rename to src/core/server/saved_objects/import/types.ts diff --git a/src/legacy/server/saved_objects/import/validate_references.test.ts b/src/core/server/saved_objects/import/validate_references.test.ts similarity index 100% rename from src/legacy/server/saved_objects/import/validate_references.test.ts rename to src/core/server/saved_objects/import/validate_references.test.ts diff --git a/src/legacy/server/saved_objects/import/validate_references.ts b/src/core/server/saved_objects/import/validate_references.ts similarity index 100% rename from src/legacy/server/saved_objects/import/validate_references.ts rename to src/core/server/saved_objects/import/validate_references.ts diff --git a/src/legacy/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts similarity index 100% rename from src/legacy/server/saved_objects/index.ts rename to src/core/server/saved_objects/index.ts diff --git a/src/legacy/server/saved_objects/management/index.ts b/src/core/server/saved_objects/management/index.ts similarity index 100% rename from src/legacy/server/saved_objects/management/index.ts rename to src/core/server/saved_objects/management/index.ts diff --git a/src/legacy/server/saved_objects/management/management.mock.ts b/src/core/server/saved_objects/management/management.mock.ts similarity index 100% rename from src/legacy/server/saved_objects/management/management.mock.ts rename to src/core/server/saved_objects/management/management.mock.ts diff --git a/src/legacy/server/saved_objects/management/management.test.ts b/src/core/server/saved_objects/management/management.test.ts similarity index 100% rename from src/legacy/server/saved_objects/management/management.test.ts rename to src/core/server/saved_objects/management/management.test.ts diff --git a/src/legacy/server/saved_objects/management/management.ts b/src/core/server/saved_objects/management/management.ts similarity index 100% rename from src/legacy/server/saved_objects/management/management.ts rename to src/core/server/saved_objects/management/management.ts diff --git a/src/legacy/server/mappings/index.ts b/src/core/server/saved_objects/mappings/index.ts similarity index 99% rename from src/legacy/server/mappings/index.ts rename to src/core/server/saved_objects/mappings/index.ts index 40bc62d42cc23..0d3bfd00c415e 100644 --- a/src/legacy/server/mappings/index.ts +++ b/src/core/server/saved_objects/mappings/index.ts @@ -16,6 +16,5 @@ * specific language governing permissions and limitations * under the License. */ - export { getTypes, getProperty, getRootProperties, getRootPropertiesObjects } from './lib'; export { FieldMapping, MappingMeta, MappingProperties, IndexMapping } from './types'; diff --git a/src/legacy/server/mappings/lib/get_property.test.ts b/src/core/server/saved_objects/mappings/lib/get_property.test.ts similarity index 100% rename from src/legacy/server/mappings/lib/get_property.test.ts rename to src/core/server/saved_objects/mappings/lib/get_property.test.ts diff --git a/src/legacy/server/mappings/lib/get_property.ts b/src/core/server/saved_objects/mappings/lib/get_property.ts similarity index 100% rename from src/legacy/server/mappings/lib/get_property.ts rename to src/core/server/saved_objects/mappings/lib/get_property.ts diff --git a/src/legacy/server/mappings/lib/get_root_properties.ts b/src/core/server/saved_objects/mappings/lib/get_root_properties.ts similarity index 100% rename from src/legacy/server/mappings/lib/get_root_properties.ts rename to src/core/server/saved_objects/mappings/lib/get_root_properties.ts diff --git a/src/legacy/server/mappings/lib/get_root_properties_objects.test.ts b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts similarity index 100% rename from src/legacy/server/mappings/lib/get_root_properties_objects.test.ts rename to src/core/server/saved_objects/mappings/lib/get_root_properties_objects.test.ts diff --git a/src/legacy/server/mappings/lib/get_root_properties_objects.ts b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts similarity index 100% rename from src/legacy/server/mappings/lib/get_root_properties_objects.ts rename to src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts diff --git a/src/legacy/server/mappings/lib/get_types.ts b/src/core/server/saved_objects/mappings/lib/get_types.ts similarity index 100% rename from src/legacy/server/mappings/lib/get_types.ts rename to src/core/server/saved_objects/mappings/lib/get_types.ts diff --git a/src/legacy/server/mappings/lib/index.ts b/src/core/server/saved_objects/mappings/lib/index.ts similarity index 100% rename from src/legacy/server/mappings/lib/index.ts rename to src/core/server/saved_objects/mappings/lib/index.ts diff --git a/src/legacy/server/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts similarity index 100% rename from src/legacy/server/mappings/types.ts rename to src/core/server/saved_objects/mappings/types.ts diff --git a/src/legacy/server/saved_objects/migrations/README.md b/src/core/server/saved_objects/migrations/README.md similarity index 100% rename from src/legacy/server/saved_objects/migrations/README.md rename to src/core/server/saved_objects/migrations/README.md diff --git a/src/legacy/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap rename to src/core/server/saved_objects/migrations/core/__snapshots__/build_active_mappings.test.ts.snap diff --git a/src/legacy/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap b/src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap rename to src/core/server/saved_objects/migrations/core/__snapshots__/elastic_index.test.ts.snap diff --git a/src/legacy/server/saved_objects/migrations/core/build_active_mappings.test.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts similarity index 99% rename from src/legacy/server/saved_objects/migrations/core/build_active_mappings.test.ts rename to src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts index d9baccfcadb80..71f589f24369a 100644 --- a/src/legacy/server/saved_objects/migrations/core/build_active_mappings.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IndexMapping } from './../../../mappings'; +import { IndexMapping } from './../../mappings'; import { buildActiveMappings, diffMappings } from './build_active_mappings'; describe('buildActiveMappings', () => { diff --git a/src/legacy/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts similarity index 98% rename from src/legacy/server/saved_objects/migrations/core/build_active_mappings.ts rename to src/core/server/saved_objects/migrations/core/build_active_mappings.ts index f28da1c5db74a..2cf640cceea83 100644 --- a/src/legacy/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -23,7 +23,7 @@ import crypto from 'crypto'; import _ from 'lodash'; -import { IndexMapping, MappingProperties } from './../../../mappings'; +import { IndexMapping, MappingProperties } from './../../mappings'; /** * Creates an index mapping with the core properties required by saved object diff --git a/src/legacy/server/saved_objects/migrations/core/build_index_map.ts b/src/core/server/saved_objects/migrations/core/build_index_map.ts similarity index 95% rename from src/legacy/server/saved_objects/migrations/core/build_index_map.ts rename to src/core/server/saved_objects/migrations/core/build_index_map.ts index 5c0f08bf4046b..365c79692ba0d 100644 --- a/src/legacy/server/saved_objects/migrations/core/build_index_map.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.ts @@ -17,7 +17,7 @@ * under the License. */ -import { MappingProperties } from 'src/legacy/server/mappings'; +import { MappingProperties } from '../../mappings'; import { SavedObjectsSchemaDefinition } from '../../schema'; /* diff --git a/src/legacy/server/saved_objects/migrations/core/call_cluster.ts b/src/core/server/saved_objects/migrations/core/call_cluster.ts similarity index 99% rename from src/legacy/server/saved_objects/migrations/core/call_cluster.ts rename to src/core/server/saved_objects/migrations/core/call_cluster.ts index 961961597855a..f5b4f787a61d4 100644 --- a/src/legacy/server/saved_objects/migrations/core/call_cluster.ts +++ b/src/core/server/saved_objects/migrations/core/call_cluster.ts @@ -23,7 +23,7 @@ * funcationality contained here. */ -import { IndexMapping } from '../../../mappings'; +import { IndexMapping } from '../../mappings'; /* eslint-disable @typescript-eslint/unified-signatures */ export interface CallCluster { diff --git a/src/legacy/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/document_migrator.test.ts rename to src/core/server/saved_objects/migrations/core/document_migrator.test.ts diff --git a/src/legacy/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts similarity index 97% rename from src/legacy/server/saved_objects/migrations/core/document_migrator.ts rename to src/core/server/saved_objects/migrations/core/document_migrator.ts index bcff2988f4afe..578fe49b2d3cc 100644 --- a/src/legacy/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -65,7 +65,7 @@ import _ from 'lodash'; import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; import { RawSavedObjectDoc } from '../../serialization'; -import { MigrationVersion } from '../../'; +import { SavedObjectsMigrationVersion } from '../../'; import { LogFn, Logger, MigrationLogger } from './migration_logger'; export type TransformFn = (doc: RawSavedObjectDoc, log?: Logger) => RawSavedObjectDoc; @@ -97,7 +97,7 @@ interface ActiveMigrations { * Manages migration of individual documents. */ export interface VersionedTransformer { - migrationVersion: MigrationVersion; + migrationVersion: SavedObjectsMigrationVersion; migrate: TransformFn; } @@ -134,10 +134,10 @@ export class DocumentMigrator implements VersionedTransformer { * Gets the latest version of each migratable property. * * @readonly - * @type {MigrationVersion} + * @type {SavedObjectsMigrationVersion} * @memberof DocumentMigrator */ - public get migrationVersion(): MigrationVersion { + public get migrationVersion(): SavedObjectsMigrationVersion { return _.mapValues(this.migrations, ({ latestVersion }) => latestVersion); } @@ -387,7 +387,7 @@ function applicableTransforms(migrations: ActiveMigrations, doc: RawSavedObjectD */ function updateMigrationVersion( doc: RawSavedObjectDoc, - migrationVersion: MigrationVersion, + migrationVersion: SavedObjectsMigrationVersion, prop: string, version: string ) { @@ -403,7 +403,7 @@ function updateMigrationVersion( */ function assertNoDowngrades( doc: RawSavedObjectDoc, - migrationVersion: MigrationVersion, + migrationVersion: SavedObjectsMigrationVersion, prop: string, version: string ) { diff --git a/src/legacy/server/saved_objects/migrations/core/elastic_index.test.ts b/src/core/server/saved_objects/migrations/core/elastic_index.test.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/elastic_index.test.ts rename to src/core/server/saved_objects/migrations/core/elastic_index.test.ts diff --git a/src/legacy/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts similarity index 97% rename from src/legacy/server/saved_objects/migrations/core/elastic_index.ts rename to src/core/server/saved_objects/migrations/core/elastic_index.ts index 1e55bd3d01688..9606a46edef95 100644 --- a/src/legacy/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -23,8 +23,8 @@ */ import _ from 'lodash'; -import { IndexMapping } from '../../../mappings'; -import { MigrationVersion } from '../../'; +import { IndexMapping } from '../../mappings'; +import { SavedObjectsMigrationVersion } from '../../'; import { AliasAction, CallCluster, NotFound, RawDoc, ShardsInfo } from './call_cluster'; const settings = { number_of_shards: 1, auto_expand_replicas: '0-1' }; @@ -147,12 +147,12 @@ export async function write(callCluster: CallCluster, index: string, docs: RawDo * * @param {CallCluster} callCluster * @param {string} index - * @param {MigrationVersion} migrationVersion - The latest versions of the migrations + * @param {SavedObjectsMigrationVersion} migrationVersion - The latest versions of the migrations */ export async function migrationsUpToDate( callCluster: CallCluster, index: string, - migrationVersion: MigrationVersion, + migrationVersion: SavedObjectsMigrationVersion, retryCount: number = 10 ): Promise { try { diff --git a/src/legacy/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/index.ts rename to src/core/server/saved_objects/migrations/core/index.ts diff --git a/src/legacy/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/index_migrator.test.ts rename to src/core/server/saved_objects/migrations/core/index_migrator.test.ts diff --git a/src/legacy/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/index_migrator.ts rename to src/core/server/saved_objects/migrations/core/index_migrator.ts diff --git a/src/legacy/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/migrate_raw_docs.test.ts rename to src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts diff --git a/src/legacy/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/migrate_raw_docs.ts rename to src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts diff --git a/src/legacy/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts similarity index 98% rename from src/legacy/server/saved_objects/migrations/core/migration_context.ts rename to src/core/server/saved_objects/migrations/core/migration_context.ts index 33d1b9635f54f..f3c4b271c3a72 100644 --- a/src/legacy/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -25,7 +25,7 @@ */ import { SavedObjectsSerializer } from '../../serialization'; -import { MappingProperties } from './../../../mappings'; +import { MappingProperties } from '../../mappings'; import { buildActiveMappings } from './build_active_mappings'; import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; diff --git a/src/legacy/server/saved_objects/migrations/core/migration_coordinator.test.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/migration_coordinator.test.ts rename to src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts diff --git a/src/legacy/server/saved_objects/migrations/core/migration_coordinator.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/migration_coordinator.ts rename to src/core/server/saved_objects/migrations/core/migration_coordinator.ts diff --git a/src/legacy/server/saved_objects/migrations/core/migration_logger.ts b/src/core/server/saved_objects/migrations/core/migration_logger.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/core/migration_logger.ts rename to src/core/server/saved_objects/migrations/core/migration_logger.ts diff --git a/src/legacy/server/saved_objects/migrations/index.ts b/src/core/server/saved_objects/migrations/index.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/index.ts rename to src/core/server/saved_objects/migrations/index.ts diff --git a/src/legacy/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap similarity index 100% rename from src/legacy/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap rename to src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap diff --git a/src/legacy/server/saved_objects/migrations/kibana/index.ts b/src/core/server/saved_objects/migrations/kibana/index.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/kibana/index.ts rename to src/core/server/saved_objects/migrations/kibana/index.ts diff --git a/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts similarity index 100% rename from src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.test.ts rename to src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts diff --git a/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts similarity index 99% rename from src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.ts rename to src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 69322ef0a8b23..ebc8e90871970 100644 --- a/src/legacy/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -23,7 +23,7 @@ */ import { once } from 'lodash'; -import { MappingProperties } from '../../../mappings'; +import { MappingProperties } from '../../mappings'; import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from '../../schema'; import { SavedObjectsManagementDefinition } from '../../management'; import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; diff --git a/src/legacy/server/saved_objects/schema/index.ts b/src/core/server/saved_objects/schema/index.ts similarity index 100% rename from src/legacy/server/saved_objects/schema/index.ts rename to src/core/server/saved_objects/schema/index.ts diff --git a/src/legacy/server/saved_objects/schema/schema.mock.ts b/src/core/server/saved_objects/schema/schema.mock.ts similarity index 100% rename from src/legacy/server/saved_objects/schema/schema.mock.ts rename to src/core/server/saved_objects/schema/schema.mock.ts diff --git a/src/legacy/server/saved_objects/schema/schema.test.ts b/src/core/server/saved_objects/schema/schema.test.ts similarity index 100% rename from src/legacy/server/saved_objects/schema/schema.test.ts rename to src/core/server/saved_objects/schema/schema.test.ts diff --git a/src/legacy/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts similarity index 100% rename from src/legacy/server/saved_objects/schema/schema.ts rename to src/core/server/saved_objects/schema/schema.ts diff --git a/src/legacy/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts similarity index 97% rename from src/legacy/server/saved_objects/serialization/index.ts rename to src/core/server/saved_objects/serialization/index.ts index 72071ae8866fa..86a448ba8a5be 100644 --- a/src/legacy/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -27,7 +27,10 @@ import uuid from 'uuid'; import { SavedObjectsSchema } from '../schema'; import { decodeVersion, encodeVersion } from '../version'; -import { MigrationVersion, SavedObjectReference } from '../service/saved_objects_client'; +import { + SavedObjectsMigrationVersion, + SavedObjectReference, +} from '../service/saved_objects_client'; /** * A raw document as represented directly in the saved object index. @@ -51,7 +54,7 @@ interface SavedObjectDoc { id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; - migrationVersion?: MigrationVersion; + migrationVersion?: SavedObjectsMigrationVersion; version?: string; updated_at?: string; diff --git a/src/legacy/server/saved_objects/serialization/serialization.test.ts b/src/core/server/saved_objects/serialization/serialization.test.ts similarity index 100% rename from src/legacy/server/saved_objects/serialization/serialization.test.ts rename to src/core/server/saved_objects/serialization/serialization.test.ts diff --git a/src/legacy/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts similarity index 94% rename from src/legacy/server/saved_objects/service/index.ts rename to src/core/server/saved_objects/service/index.ts index c4e0d66eb95b8..697e1d2d41471 100644 --- a/src/legacy/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -20,6 +20,9 @@ import { ScopedSavedObjectsClientProvider } from './lib'; import { SavedObjectsClient } from './saved_objects_client'; +/** + * @public + */ export interface SavedObjectsService { // ATTENTION: these types are incomplete addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider< @@ -35,6 +38,8 @@ export { SavedObjectsRepository, ScopedSavedObjectsClientProvider, SavedObjectsClientWrapperFactory, + SavedObjectsClientWrapperOptions, + SavedObjectsErrorHelpers, } from './lib'; export * from './saved_objects_client'; diff --git a/src/legacy/server/saved_objects/service/lib/__snapshots__/priority_collection.test.ts.snap b/src/core/server/saved_objects/service/lib/__snapshots__/priority_collection.test.ts.snap similarity index 100% rename from src/legacy/server/saved_objects/service/lib/__snapshots__/priority_collection.test.ts.snap rename to src/core/server/saved_objects/service/lib/__snapshots__/priority_collection.test.ts.snap diff --git a/src/legacy/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap b/src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap similarity index 100% rename from src/legacy/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap rename to src/core/server/saved_objects/service/lib/__snapshots__/repository.test.js.snap diff --git a/src/legacy/server/saved_objects/service/lib/__snapshots__/scoped_client_provider.test.js.snap b/src/core/server/saved_objects/service/lib/__snapshots__/scoped_client_provider.test.js.snap similarity index 100% rename from src/legacy/server/saved_objects/service/lib/__snapshots__/scoped_client_provider.test.js.snap rename to src/core/server/saved_objects/service/lib/__snapshots__/scoped_client_provider.test.js.snap diff --git a/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts similarity index 66% rename from src/legacy/server/saved_objects/service/lib/decorate_es_error.test.ts rename to src/core/server/saved_objects/service/lib/decorate_es_error.test.ts index 272a26327b808..2fd9b487f470a 100644 --- a/src/legacy/server/saved_objects/service/lib/decorate_es_error.test.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.test.ts @@ -20,15 +20,7 @@ import { errors as esErrors } from 'elasticsearch'; import { decorateEsError } from './decorate_es_error'; -import { - isBadRequestError, - isConflictError, - isEsUnavailableError, - isForbiddenError, - isNotAuthorizedError, - isNotFoundError, - isRequestEntityTooLargeError, -} from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; describe('savedObjectsClient/decorateEsError', () => { it('always returns the same error it receives', () => { @@ -38,74 +30,74 @@ describe('savedObjectsClient/decorateEsError', () => { it('makes es.ConnectionFault a SavedObjectsClient/EsUnavailable error', () => { const error = new esErrors.ConnectionFault(); - expect(isEsUnavailableError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); it('makes es.ServiceUnavailable a SavedObjectsClient/EsUnavailable error', () => { const error = new esErrors.ServiceUnavailable(); - expect(isEsUnavailableError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); it('makes es.NoConnections a SavedObjectsClient/EsUnavailable error', () => { const error = new esErrors.NoConnections(); - expect(isEsUnavailableError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); it('makes es.RequestTimeout a SavedObjectsClient/EsUnavailable error', () => { const error = new esErrors.RequestTimeout(); - expect(isEsUnavailableError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isEsUnavailableError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); it('makes es.Conflict a SavedObjectsClient/Conflict error', () => { const error = new esErrors.Conflict(); - expect(isConflictError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isConflictError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(true); }); it('makes es.AuthenticationException a SavedObjectsClient/NotAuthorized error', () => { const error = new esErrors.AuthenticationException(); - expect(isNotAuthorizedError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isNotAuthorizedError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(true); }); it('makes es.Forbidden a SavedObjectsClient/Forbidden error', () => { const error = new esErrors.Forbidden(); - expect(isForbiddenError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isForbiddenError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true); }); it('makes es.RequestEntityTooLarge a SavedObjectsClient/RequestEntityTooLarge error', () => { const error = new esErrors.RequestEntityTooLarge(); - expect(isRequestEntityTooLargeError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isRequestEntityTooLargeError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isRequestEntityTooLargeError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isRequestEntityTooLargeError(error)).toBe(true); }); it('discards es.NotFound errors and returns a generic NotFound error', () => { const error = new esErrors.NotFound(); - expect(isNotFoundError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(false); const genericError = decorateEsError(error); expect(genericError).not.toBe(error); - expect(isNotFoundError(error)).toBe(false); - expect(isNotFoundError(genericError)).toBe(true); + expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isNotFoundError(genericError)).toBe(true); }); it('makes es.BadRequest a SavedObjectsClient/BadRequest error', () => { const error = new esErrors.BadRequest(); - expect(isBadRequestError(error)).toBe(false); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); expect(decorateEsError(error)).toBe(error); - expect(isBadRequestError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); }); it('returns other errors as Boom errors', () => { diff --git a/src/legacy/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts similarity index 72% rename from src/legacy/server/saved_objects/service/lib/decorate_es_error.ts rename to src/core/server/saved_objects/service/lib/decorate_es_error.ts index becb41b78dad4..af66348a98eb3 100644 --- a/src/legacy/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -36,16 +36,7 @@ const { BadRequest, } = elasticsearch.errors; -import { - createGenericNotFoundError, - decorateBadRequestError, - decorateConflictError, - decorateEsUnavailableError, - decorateForbiddenError, - decorateGeneralError, - decorateNotAuthorizedError, - decorateRequestEntityTooLargeError, -} from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; export function decorateEsError(error: Error) { if (!(error instanceof Error)) { @@ -59,32 +50,32 @@ export function decorateEsError(error: Error) { error instanceof NoConnections || error instanceof RequestTimeout ) { - return decorateEsUnavailableError(error, reason); + return SavedObjectsErrorHelpers.decorateEsUnavailableError(error, reason); } if (error instanceof Conflict) { - return decorateConflictError(error, reason); + return SavedObjectsErrorHelpers.decorateConflictError(error, reason); } if (error instanceof NotAuthorized) { - return decorateNotAuthorizedError(error, reason); + return SavedObjectsErrorHelpers.decorateNotAuthorizedError(error, reason); } if (error instanceof Forbidden) { - return decorateForbiddenError(error, reason); + return SavedObjectsErrorHelpers.decorateForbiddenError(error, reason); } if (error instanceof RequestEntityTooLarge) { - return decorateRequestEntityTooLargeError(error, reason); + return SavedObjectsErrorHelpers.decorateRequestEntityTooLargeError(error, reason); } if (error instanceof NotFound) { - return createGenericNotFoundError(); + return SavedObjectsErrorHelpers.createGenericNotFoundError(); } if (error instanceof BadRequest) { - return decorateBadRequestError(error, reason); + return SavedObjectsErrorHelpers.decorateBadRequestError(error, reason); } - return decorateGeneralError(error, reason); + return SavedObjectsErrorHelpers.decorateGeneralError(error, reason); } diff --git a/src/legacy/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts similarity index 63% rename from src/legacy/server/saved_objects/service/lib/errors.test.ts rename to src/core/server/saved_objects/service/lib/errors.test.ts index facbacae84d07..12fc913f93090 100644 --- a/src/legacy/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -19,30 +19,12 @@ import Boom from 'boom'; -import { - createBadRequestError, - createEsAutoCreateIndexError, - createGenericNotFoundError, - createUnsupportedTypeError, - decorateBadRequestError, - decorateConflictError, - decorateEsUnavailableError, - decorateForbiddenError, - decorateGeneralError, - decorateNotAuthorizedError, - isBadRequestError, - isConflictError, - isEsAutoCreateIndexError, - isEsUnavailableError, - isForbiddenError, - isNotAuthorizedError, - isNotFoundError, -} from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; describe('savedObjectsClient/errorTypes', () => { describe('BadRequest error', () => { describe('createUnsupportedTypeError', () => { - const errorObj = createUnsupportedTypeError('someType'); + const errorObj = SavedObjectsErrorHelpers.createUnsupportedTypeError('someType'); it('should have the unsupported type message', () => { expect(errorObj).toHaveProperty( @@ -60,18 +42,18 @@ describe('savedObjectsClient/errorTypes', () => { }); it("should be identified by 'isBadRequestError' method", () => { - expect(isBadRequestError(errorObj)).toBeTruthy(); + expect(SavedObjectsErrorHelpers.isBadRequestError(errorObj)).toBeTruthy(); }); }); describe('createBadRequestError', () => { - const errorObj = createBadRequestError('test reason message'); + const errorObj = SavedObjectsErrorHelpers.createBadRequestError('test reason message'); it('should create an appropriately structured error object', () => { expect(errorObj.message).toEqual('test reason message: Bad Request'); }); it("should be identified by 'isBadRequestError' method", () => { - expect(isBadRequestError(errorObj)).toBeTruthy(); + expect(SavedObjectsErrorHelpers.isBadRequestError(errorObj)).toBeTruthy(); }); it('has boom properties', () => { @@ -86,39 +68,42 @@ describe('savedObjectsClient/errorTypes', () => { describe('decorateBadRequestError', () => { it('returns original object', () => { const error = new Error(); - expect(decorateBadRequestError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.decorateBadRequestError(error)).toBe(error); }); it('makes the error identifiable as a BadRequest error', () => { const error = new Error(); - expect(isBadRequestError(error)).toBe(false); - decorateBadRequestError(error); - expect(isBadRequestError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateBadRequestError(error); + expect(SavedObjectsErrorHelpers.isBadRequestError(error)).toBe(true); }); it('adds boom properties', () => { - const error = decorateBadRequestError(new Error()); + const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error()); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(400); }); it('preserves boom properties of input', () => { const error = Boom.notFound(); - decorateBadRequestError(error); + SavedObjectsErrorHelpers.decorateBadRequestError(error); expect(error.output.statusCode).toBe(404); }); describe('error.output', () => { it('defaults to message of error', () => { - const error = decorateBadRequestError(new Error('foobar')); + const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); it('prefixes message with passed reason', () => { - const error = decorateBadRequestError(new Error('foobar'), 'biz'); + const error = SavedObjectsErrorHelpers.decorateBadRequestError( + new Error('foobar'), + 'biz' + ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); it('sets statusCode to 400', () => { - const error = decorateBadRequestError(new Error('foo')); + const error = SavedObjectsErrorHelpers.decorateBadRequestError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 400); }); }); @@ -128,39 +113,42 @@ describe('savedObjectsClient/errorTypes', () => { describe('decorateNotAuthorizedError', () => { it('returns original object', () => { const error = new Error(); - expect(decorateNotAuthorizedError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.decorateNotAuthorizedError(error)).toBe(error); }); it('makes the error identifiable as a NotAuthorized error', () => { const error = new Error(); - expect(isNotAuthorizedError(error)).toBe(false); - decorateNotAuthorizedError(error); - expect(isNotAuthorizedError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateNotAuthorizedError(error); + expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(true); }); it('adds boom properties', () => { - const error = decorateNotAuthorizedError(new Error()); + const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error()); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(401); }); it('preserves boom properties of input', () => { const error = Boom.notFound(); - decorateNotAuthorizedError(error); + SavedObjectsErrorHelpers.decorateNotAuthorizedError(error); expect(error.output.statusCode).toBe(404); }); describe('error.output', () => { it('defaults to message of error', () => { - const error = decorateNotAuthorizedError(new Error('foobar')); + const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); it('prefixes message with passed reason', () => { - const error = decorateNotAuthorizedError(new Error('foobar'), 'biz'); + const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError( + new Error('foobar'), + 'biz' + ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); it('sets statusCode to 401', () => { - const error = decorateNotAuthorizedError(new Error('foo')); + const error = SavedObjectsErrorHelpers.decorateNotAuthorizedError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 401); }); }); @@ -170,39 +158,39 @@ describe('savedObjectsClient/errorTypes', () => { describe('decorateForbiddenError', () => { it('returns original object', () => { const error = new Error(); - expect(decorateForbiddenError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.decorateForbiddenError(error)).toBe(error); }); it('makes the error identifiable as a Forbidden error', () => { const error = new Error(); - expect(isForbiddenError(error)).toBe(false); - decorateForbiddenError(error); - expect(isForbiddenError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateForbiddenError(error); + expect(SavedObjectsErrorHelpers.isForbiddenError(error)).toBe(true); }); it('adds boom properties', () => { - const error = decorateForbiddenError(new Error()); + const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error()); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(403); }); it('preserves boom properties of input', () => { const error = Boom.notFound(); - decorateForbiddenError(error); + SavedObjectsErrorHelpers.decorateForbiddenError(error); expect(error.output.statusCode).toBe(404); }); describe('error.output', () => { it('defaults to message of error', () => { - const error = decorateForbiddenError(new Error('foobar')); + const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); it('prefixes message with passed reason', () => { - const error = decorateForbiddenError(new Error('foobar'), 'biz'); + const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foobar'), 'biz'); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); it('sets statusCode to 403', () => { - const error = decorateForbiddenError(new Error('foo')); + const error = SavedObjectsErrorHelpers.decorateForbiddenError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 403); }); }); @@ -211,12 +199,12 @@ describe('savedObjectsClient/errorTypes', () => { describe('NotFound error', () => { describe('createGenericNotFoundError', () => { it('makes an error identifiable as a NotFound error', () => { - const error = createGenericNotFoundError(); - expect(isNotFoundError(error)).toBe(true); + const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); + expect(SavedObjectsErrorHelpers.isNotFoundError(error)).toBe(true); }); it('is a boom error, has boom properties', () => { - const error = createGenericNotFoundError(); + const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); expect(error).toHaveProperty('isBoom'); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(404); @@ -224,11 +212,11 @@ describe('savedObjectsClient/errorTypes', () => { describe('error.output', () => { it('Uses "Not Found" message', () => { - const error = createGenericNotFoundError(); + const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); expect(error.output.payload).toHaveProperty('message', 'Not Found'); }); it('sets statusCode to 404', () => { - const error = createGenericNotFoundError(); + const error = SavedObjectsErrorHelpers.createGenericNotFoundError(); expect(error.output).toHaveProperty('statusCode', 404); }); }); @@ -238,39 +226,39 @@ describe('savedObjectsClient/errorTypes', () => { describe('decorateConflictError', () => { it('returns original object', () => { const error = new Error(); - expect(decorateConflictError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.decorateConflictError(error)).toBe(error); }); it('makes the error identifiable as a Conflict error', () => { const error = new Error(); - expect(isConflictError(error)).toBe(false); - decorateConflictError(error); - expect(isConflictError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateConflictError(error); + expect(SavedObjectsErrorHelpers.isConflictError(error)).toBe(true); }); it('adds boom properties', () => { - const error = decorateConflictError(new Error()); + const error = SavedObjectsErrorHelpers.decorateConflictError(new Error()); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(409); }); it('preserves boom properties of input', () => { const error = Boom.notFound(); - decorateConflictError(error); + SavedObjectsErrorHelpers.decorateConflictError(error); expect(error.output.statusCode).toBe(404); }); describe('error.output', () => { it('defaults to message of error', () => { - const error = decorateConflictError(new Error('foobar')); + const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); it('prefixes message with passed reason', () => { - const error = decorateConflictError(new Error('foobar'), 'biz'); + const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foobar'), 'biz'); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); it('sets statusCode to 409', () => { - const error = decorateConflictError(new Error('foo')); + const error = SavedObjectsErrorHelpers.decorateConflictError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 409); }); }); @@ -280,39 +268,42 @@ describe('savedObjectsClient/errorTypes', () => { describe('decorateEsUnavailableError', () => { it('returns original object', () => { const error = new Error(); - expect(decorateEsUnavailableError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.decorateEsUnavailableError(error)).toBe(error); }); it('makes the error identifiable as a EsUnavailable error', () => { const error = new Error(); - expect(isEsUnavailableError(error)).toBe(false); - decorateEsUnavailableError(error); - expect(isEsUnavailableError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(false); + SavedObjectsErrorHelpers.decorateEsUnavailableError(error); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); }); it('adds boom properties', () => { - const error = decorateEsUnavailableError(new Error()); + const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error()); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(503); }); it('preserves boom properties of input', () => { const error = Boom.notFound(); - decorateEsUnavailableError(error); + SavedObjectsErrorHelpers.decorateEsUnavailableError(error); expect(error.output.statusCode).toBe(404); }); describe('error.output', () => { it('defaults to message of error', () => { - const error = decorateEsUnavailableError(new Error('foobar')); + const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foobar')); expect(error.output.payload).toHaveProperty('message', 'foobar'); }); it('prefixes message with passed reason', () => { - const error = decorateEsUnavailableError(new Error('foobar'), 'biz'); + const error = SavedObjectsErrorHelpers.decorateEsUnavailableError( + new Error('foobar'), + 'biz' + ); expect(error.output.payload).toHaveProperty('message', 'biz: foobar'); }); it('sets statusCode to 503', () => { - const error = decorateEsUnavailableError(new Error('foo')); + const error = SavedObjectsErrorHelpers.decorateEsUnavailableError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 503); }); }); @@ -322,28 +313,28 @@ describe('savedObjectsClient/errorTypes', () => { describe('decorateGeneralError', () => { it('returns original object', () => { const error = new Error(); - expect(decorateGeneralError(error)).toBe(error); + expect(SavedObjectsErrorHelpers.decorateGeneralError(error)).toBe(error); }); it('adds boom properties', () => { - const error = decorateGeneralError(new Error()); + const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error()); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(500); }); it('preserves boom properties of input', () => { const error = Boom.notFound(); - decorateGeneralError(error); + SavedObjectsErrorHelpers.decorateGeneralError(error); expect(error.output.statusCode).toBe(404); }); describe('error.output', () => { it('ignores error message', () => { - const error = decorateGeneralError(new Error('foobar')); + const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foobar')); expect(error.output.payload.message).toMatch(/internal server error/i); }); it('sets statusCode to 500', () => { - const error = decorateGeneralError(new Error('foo')); + const error = SavedObjectsErrorHelpers.decorateGeneralError(new Error('foo')); expect(error.output).toHaveProperty('statusCode', 500); }); }); @@ -355,19 +346,23 @@ describe('savedObjectsClient/errorTypes', () => { it('does not take an error argument', () => { const error = new Error(); // @ts-ignore - expect(createEsAutoCreateIndexError(error)).not.toBe(error); + expect(SavedObjectsErrorHelpers.createEsAutoCreateIndexError(error)).not.toBe(error); }); it('returns a new Error', () => { - expect(createEsAutoCreateIndexError()).toBeInstanceOf(Error); + expect(SavedObjectsErrorHelpers.createEsAutoCreateIndexError()).toBeInstanceOf(Error); }); it('makes errors identifiable as EsAutoCreateIndex errors', () => { - expect(isEsAutoCreateIndexError(createEsAutoCreateIndexError())).toBe(true); + expect( + SavedObjectsErrorHelpers.isEsAutoCreateIndexError( + SavedObjectsErrorHelpers.createEsAutoCreateIndexError() + ) + ).toBe(true); }); it('returns a boom error', () => { - const error = createEsAutoCreateIndexError(); + const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); expect(error).toHaveProperty('isBoom'); expect(typeof error.output).toBe('object'); expect(error.output.statusCode).toBe(503); @@ -375,11 +370,11 @@ describe('savedObjectsClient/errorTypes', () => { describe('error.output', () => { it('uses "Automatic index creation failed" message', () => { - const error = createEsAutoCreateIndexError(); + const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed'); }); it('sets statusCode to 503', () => { - const error = createEsAutoCreateIndexError(); + const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); expect(error.output).toHaveProperty('statusCode', 503); }); }); diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts new file mode 100644 index 0000000000000..e9138e9b8a347 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -0,0 +1,182 @@ +/* + * 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 Boom from 'boom'; + +// 400 - badRequest +const CODE_BAD_REQUEST = 'SavedObjectsClient/badRequest'; +// 400 - invalid version +const CODE_INVALID_VERSION = 'SavedObjectsClient/invalidVersion'; +// 401 - Not Authorized +const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized'; +// 403 - Forbidden +const CODE_FORBIDDEN = 'SavedObjectsClient/forbidden'; +// 413 - Request Entity Too Large +const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge'; +// 404 - Not Found +const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; +// 409 - Conflict +const CODE_CONFLICT = 'SavedObjectsClient/conflict'; +// 503 - Es Unavailable +const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; +// 503 - Unable to automatically create index because of action.auto_create_index setting +const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex'; +// 500 - General Error +const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError'; + +const code = Symbol('SavedObjectsClientErrorCode'); + +export interface DecoratedError extends Boom { + [code]?: string; +} + +function decorate( + error: Error | DecoratedError, + errorCode: string, + statusCode: number, + message?: string +): DecoratedError { + if (isSavedObjectsClientError(error)) { + return error; + } + + const boom = Boom.boomify(error, { + statusCode, + message, + override: false, + }) as DecoratedError; + + boom[code] = errorCode; + + return boom; +} + +function isSavedObjectsClientError(error: any): error is DecoratedError { + return Boolean(error && error[code]); +} + +function decorateBadRequestError(error: Error, reason?: string) { + return decorate(error, CODE_BAD_REQUEST, 400, reason); +} + +/** + * @public + */ +export class SavedObjectsErrorHelpers { + public static isSavedObjectsClientError(error: any): error is DecoratedError { + return isSavedObjectsClientError(error); + } + + public static decorateBadRequestError(error: Error, reason?: string) { + return decorateBadRequestError(error, reason); + } + + public static createBadRequestError(reason?: string) { + return decorateBadRequestError(new Error('Bad Request'), reason); + } + + public static createUnsupportedTypeError(type: string) { + return decorateBadRequestError( + new Error('Bad Request'), + `Unsupported saved object type: '${type}'` + ); + } + + public static isBadRequestError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_BAD_REQUEST; + } + + public static createInvalidVersionError(versionInput?: string) { + return decorate( + Boom.badRequest(`Invalid version [${versionInput}]`), + CODE_INVALID_VERSION, + 400 + ); + } + + public static isInvalidVersionError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_INVALID_VERSION; + } + + public static decorateNotAuthorizedError(error: Error, reason?: string) { + return decorate(error, CODE_NOT_AUTHORIZED, 401, reason); + } + + public static isNotAuthorizedError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_NOT_AUTHORIZED; + } + + public static decorateForbiddenError(error: Error, reason?: string) { + return decorate(error, CODE_FORBIDDEN, 403, reason); + } + + public static isForbiddenError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_FORBIDDEN; + } + + public static decorateRequestEntityTooLargeError(error: Error, reason?: string) { + return decorate(error, CODE_REQUEST_ENTITY_TOO_LARGE, 413, reason); + } + public static isRequestEntityTooLargeError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE; + } + + public static createGenericNotFoundError(type: string | null = null, id: string | null = null) { + if (type && id) { + return decorate(Boom.notFound(`Saved object [${type}/${id}] not found`), CODE_NOT_FOUND, 404); + } + return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); + } + + public static isNotFoundError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND; + } + + public static decorateConflictError(error: Error, reason?: string) { + return decorate(error, CODE_CONFLICT, 409, reason); + } + + public static isConflictError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT; + } + + public static decorateEsUnavailableError(error: Error, reason?: string) { + return decorate(error, CODE_ES_UNAVAILABLE, 503, reason); + } + + public static isEsUnavailableError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_ES_UNAVAILABLE; + } + + public static createEsAutoCreateIndexError() { + const error = Boom.serverUnavailable('Automatic index creation failed'); + error.output.payload.attributes = error.output.payload.attributes || {}; + error.output.payload.attributes.code = 'ES_AUTO_CREATE_INDEX_ERROR'; + + return decorate(error, CODE_ES_AUTO_CREATE_INDEX_ERROR, 503); + } + + public static isEsAutoCreateIndexError(error: Error | DecoratedError) { + return isSavedObjectsClientError(error) && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR; + } + + public static decorateGeneralError(error: Error, reason?: string) { + return decorate(error, CODE_GENERAL_ERROR, 500, reason); + } +} diff --git a/src/legacy/server/saved_objects/service/lib/included_fields.test.ts b/src/core/server/saved_objects/service/lib/included_fields.test.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/included_fields.test.ts rename to src/core/server/saved_objects/service/lib/included_fields.test.ts diff --git a/src/legacy/server/saved_objects/service/lib/included_fields.ts b/src/core/server/saved_objects/service/lib/included_fields.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/included_fields.ts rename to src/core/server/saved_objects/service/lib/included_fields.ts diff --git a/src/legacy/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts similarity index 94% rename from src/legacy/server/saved_objects/service/lib/index.ts rename to src/core/server/saved_objects/service/lib/index.ts index 68fa240584100..19fdc3d75f603 100644 --- a/src/legacy/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -24,5 +24,4 @@ export { ScopedSavedObjectsClientProvider, } from './scoped_client_provider'; -import * as errors from './errors'; -export { errors }; +export { SavedObjectsErrorHelpers } from './errors'; diff --git a/src/legacy/server/saved_objects/service/lib/priority_collection.test.ts b/src/core/server/saved_objects/service/lib/priority_collection.test.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/priority_collection.test.ts rename to src/core/server/saved_objects/service/lib/priority_collection.test.ts diff --git a/src/legacy/server/saved_objects/service/lib/priority_collection.ts b/src/core/server/saved_objects/service/lib/priority_collection.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/priority_collection.ts rename to src/core/server/saved_objects/service/lib/priority_collection.ts diff --git a/src/legacy/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js similarity index 96% rename from src/legacy/server/saved_objects/service/lib/repository.test.js rename to src/core/server/saved_objects/service/lib/repository.test.js index 29ccdb3b8002a..5a2e6a617fbb5 100644 --- a/src/legacy/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -20,11 +20,11 @@ import { delay } from 'bluebird'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; -import * as errors from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; import elasticsearch from 'elasticsearch'; import { SavedObjectsSchema } from '../../schema'; import { SavedObjectsSerializer } from '../../serialization'; -import { getRootPropertiesObjects } from '../../../mappings/lib/get_root_properties_objects'; +import { getRootPropertiesObjects } from '../../mappings/lib/get_root_properties_objects'; import { encodeHitVersion } from '../../version'; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -1154,26 +1154,27 @@ describe('SavedObjectsRepository', () => { } }); - it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl', async () => { - callAdminCluster.mockReturnValue(namespacedSearchResults); - const relevantOpts = { - namespace: 'foo-namespace', - search: 'foo*', - searchFields: ['foo'], - type: ['bar'], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - }; + it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl', + async () => { + callAdminCluster.mockReturnValue(namespacedSearchResults); + const relevantOpts = { + namespace: 'foo-namespace', + search: 'foo*', + searchFields: ['foo'], + type: ['bar'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + }; - await savedObjectsRepository.find(relevantOpts); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, schema, relevantOpts); - }); + await savedObjectsRepository.find(relevantOpts); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, schema, relevantOpts); + }); it('merges output of getSearchDsl into es request body', async () => { callAdminCluster.mockReturnValue(noNamespaceSearchResults); @@ -1627,7 +1628,7 @@ describe('SavedObjectsRepository', () => { { error: { error: 'Bad Request', - message: "Unsupported saved object type: 'invalidtype': Bad Request", + message: 'Unsupported saved object type: \'invalidtype\': Bad Request', statusCode: 400, }, id: 'two', @@ -1636,7 +1637,7 @@ describe('SavedObjectsRepository', () => { { error: { error: 'Bad Request', - message: "Unsupported saved object type: 'invalidtype': Bad Request", + message: 'Unsupported saved object type: \'invalidtype\': Bad Request', statusCode: 400, }, id: 'four', @@ -2072,7 +2073,7 @@ describe('SavedObjectsRepository', () => { expect.assertions(4); const es401 = new elasticsearch.errors[401](); - expect(errors.isNotAuthorizedError(es401)).toBe(false); + expect(SavedObjectsErrorHelpers.isNotAuthorizedError(es401)).toBe(false); onBeforeWrite.mockImplementation(() => { throw es401; }); @@ -2082,13 +2083,13 @@ describe('SavedObjectsRepository', () => { } catch (error) { expect(onBeforeWrite).toHaveBeenCalledTimes(1); expect(error).toBe(es401); - expect(errors.isNotAuthorizedError(error)).toBe(true); + expect(SavedObjectsErrorHelpers.isNotAuthorizedError(error)).toBe(true); } }); }); describe('types on custom index', () => { - it("should error when attempting to 'update' an unsupported type", async () => { + it('should error when attempting to \'update\' an unsupported type', async () => { await expect( savedObjectsRepository.update('hiddenType', 'bogus', { title: 'some title' }) ).rejects.toEqual(new Error('Saved object [hiddenType/bogus] not found')); @@ -2096,25 +2097,25 @@ describe('SavedObjectsRepository', () => { }); describe('unsupported types', () => { - it("should error when attempting to 'update' an unsupported type", async () => { + it('should error when attempting to \'update\' an unsupported type', async () => { await expect( savedObjectsRepository.update('hiddenType', 'bogus', { title: 'some title' }) ).rejects.toEqual(new Error('Saved object [hiddenType/bogus] not found')); }); - it("should error when attempting to 'get' an unsupported type", async () => { + it('should error when attempting to \'get\' an unsupported type', async () => { await expect(savedObjectsRepository.get('hiddenType')).rejects.toEqual( new Error('Not Found') ); }); - it("should return an error object when attempting to 'create' an unsupported type", async () => { + it('should return an error object when attempting to \'create\' an unsupported type', async () => { await expect( savedObjectsRepository.create('hiddenType', { title: 'some title' }) - ).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request")); + ).rejects.toEqual(new Error('Unsupported saved object type: \'hiddenType\': Bad Request')); }); - it("should not return hidden saved ojects when attempting to 'find' support and unsupported types", async () => { + it('should not return hidden saved ojects when attempting to \'find\' support and unsupported types', async () => { callAdminCluster.mockReturnValue({ hits: { total: 1, @@ -2146,7 +2147,7 @@ describe('SavedObjectsRepository', () => { }); }); - it("should return empty results when attempting to 'find' an unsupported type", async () => { + it('should return empty results when attempting to \'find\' an unsupported type', async () => { callAdminCluster.mockReturnValue({ hits: { total: 0, @@ -2162,7 +2163,7 @@ describe('SavedObjectsRepository', () => { }); }); - it("should return empty results when attempting to 'find' more than one unsupported types", async () => { + it('should return empty results when attempting to \'find\' more than one unsupported types', async () => { const findParams = { type: ['hiddenType', 'hiddenType2'] }; callAdminCluster.mockReturnValue({ status: 200, @@ -2180,13 +2181,13 @@ describe('SavedObjectsRepository', () => { }); }); - it("should error when attempting to 'delete' hidden types", async () => { + it('should error when attempting to \'delete\' hidden types', async () => { await expect(savedObjectsRepository.delete('hiddenType')).rejects.toEqual( new Error('Not Found') ); }); - it("should error when attempting to 'bulkCreate' an unsupported type", async () => { + it('should error when attempting to \'bulkCreate\' an unsupported type', async () => { callAdminCluster.mockReturnValue({ items: [ { @@ -2219,7 +2220,7 @@ describe('SavedObjectsRepository', () => { { error: { error: 'Bad Request', - message: "Unsupported saved object type: 'hiddenType': Bad Request", + message: 'Unsupported saved object type: \'hiddenType\': Bad Request', statusCode: 400, }, id: 'two', @@ -2229,10 +2230,10 @@ describe('SavedObjectsRepository', () => { }); }); - it("should error when attempting to 'incrementCounter' for an unsupported type", async () => { + it('should error when attempting to \'incrementCounter\' for an unsupported type', async () => { await expect( savedObjectsRepository.incrementCounter('hiddenType', 'doesntmatter', 'fieldArg') - ).rejects.toEqual(new Error("Unsupported saved object type: 'hiddenType': Bad Request")); + ).rejects.toEqual(new Error('Unsupported saved object type: \'hiddenType\': Bad Request')); }); }); }); diff --git a/src/legacy/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts similarity index 91% rename from src/legacy/server/saved_objects/service/lib/repository.ts rename to src/core/server/saved_objects/service/lib/repository.ts index ef4b17f5106c9..eb41df3a19d2d 100644 --- a/src/legacy/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -19,28 +19,28 @@ import { omit } from 'lodash'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; -import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; +import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { decorateEsError } from './decorate_es_error'; -import * as errors from './errors'; +import { SavedObjectsErrorHelpers } from './errors'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; import { SavedObjectsSerializer, SanitizedSavedObjectDoc, RawDoc } from '../../serialization'; import { - BulkCreateObject, - CreateOptions, SavedObject, - FindOptions, SavedObjectAttributes, - FindResponse, - BulkGetObject, - BulkResponse, - UpdateOptions, - BaseOptions, - MigrationVersion, - UpdateResponse, + SavedObjectsBaseOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkResponse, + SavedObjectsCreateOptions, + SavedObjectsFindOptions, + SavedObjectsFindResponse, + SavedObjectsMigrationVersion, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, } from '../saved_objects_client'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository @@ -73,8 +73,8 @@ export interface SavedObjectsRepositoryOptions { onBeforeWrite?: (...args: Parameters) => Promise; } -export interface IncrementCounterOptions extends BaseOptions { - migrationVersion?: MigrationVersion; +export interface IncrementCounterOptions extends SavedObjectsBaseOptions { + migrationVersion?: SavedObjectsMigrationVersion; } export class SavedObjectsRepository { @@ -141,12 +141,12 @@ export class SavedObjectsRepository { public async create( type: string, attributes: T, - options: CreateOptions = { overwrite: false, references: [] } + options: SavedObjectsCreateOptions = { overwrite: false, references: [] } ): Promise> { const { id, migrationVersion, overwrite, namespace, references } = options; if (!this._allowedTypes.includes(type)) { - throw errors.createUnsupportedTypeError(type); + throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } const method = id && !overwrite ? 'create' : 'index'; @@ -177,9 +177,9 @@ export class SavedObjectsRepository { ...response, }); } catch (error) { - if (errors.isNotFoundError(error)) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // See "503s from missing index" above - throw errors.createEsAutoCreateIndexError(); + throw SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); } throw error; @@ -196,9 +196,9 @@ export class SavedObjectsRepository { * @returns {promise} - {saved_objects: [[{ id, type, version, references, attributes, error: { message } }]} */ async bulkCreate( - objects: Array>, - options: CreateOptions = {} - ): Promise> { + objects: Array>, + options: SavedObjectsCreateOptions = {} + ): Promise> { const { namespace, overwrite = false } = options; const time = this._getCurrentTime(); const bulkCreateParams: object[] = []; @@ -211,7 +211,7 @@ export class SavedObjectsRepository { error: { id: object.id, type: object.type, - error: errors.createUnsupportedTypeError(object.type).output.payload, + error: SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type).output.payload, }, }; } @@ -307,9 +307,9 @@ export class SavedObjectsRepository { * @property {string} [options.namespace] * @returns {promise} */ - async delete(type: string, id: string, options: BaseOptions = {}): Promise<{}> { + async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}): Promise<{}> { if (!this._allowedTypes.includes(type)) { - throw errors.createGenericNotFoundError(); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(); } const { namespace } = options; @@ -330,7 +330,7 @@ export class SavedObjectsRepository { const indexNotFound = response.error && response.error.type === 'index_not_found_exception'; if (docNotFound || indexNotFound) { // see "404s from missing index" above - throw errors.createGenericNotFoundError(type, id); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } throw new Error( @@ -397,7 +397,7 @@ export class SavedObjectsRepository { fields, namespace, type, - }: FindOptions): Promise> { + }: SavedObjectsFindOptions): Promise> { if (!type) { throw new TypeError(`options.type must be a string or an array of strings`); } @@ -479,9 +479,9 @@ export class SavedObjectsRepository { * ]) */ async bulkGet( - objects: BulkGetObject[] = [], - options: BaseOptions = {} - ): Promise> { + objects: SavedObjectsBulkGetObject[] = [], + options: SavedObjectsBaseOptions = {} + ): Promise> { const { namespace } = options; if (objects.length === 0) { @@ -494,7 +494,7 @@ export class SavedObjectsRepository { return ({ id, type, - error: errors.createUnsupportedTypeError(type).output.payload, + error: SavedObjectsErrorHelpers.createUnsupportedTypeError(type).output.payload, } as any) as SavedObject; }); @@ -552,10 +552,10 @@ export class SavedObjectsRepository { async get( type: string, id: string, - options: BaseOptions = {} + options: SavedObjectsBaseOptions = {} ): Promise> { if (!this._allowedTypes.includes(type)) { - throw errors.createGenericNotFoundError(type, id); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } const { namespace } = options; @@ -570,7 +570,7 @@ export class SavedObjectsRepository { const indexNotFound = response.status === 404; if (docNotFound || indexNotFound) { // see "404s from missing index" above - throw errors.createGenericNotFoundError(type, id); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } const { updated_at: updatedAt } = response._source; @@ -601,10 +601,10 @@ export class SavedObjectsRepository { type: string, id: string, attributes: Partial, - options: UpdateOptions = {} - ): Promise> { + options: SavedObjectsUpdateOptions = {} + ): Promise> { if (!this._allowedTypes.includes(type)) { - throw errors.createGenericNotFoundError(type, id); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } const { version, namespace, references = [] } = options; @@ -627,7 +627,7 @@ export class SavedObjectsRepository { if (response.status === 404) { // see "404s from missing index" above - throw errors.createGenericNotFoundError(type, id); + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } return { @@ -663,7 +663,7 @@ export class SavedObjectsRepository { throw new Error('"counterFieldName" argument must be a string'); } if (!this._allowedTypes.includes(type)) { - throw errors.createUnsupportedTypeError(type); + throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } const { migrationVersion, namespace } = options; diff --git a/src/legacy/server/saved_objects/service/lib/scoped_client_provider.test.js b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js similarity index 100% rename from src/legacy/server/saved_objects/service/lib/scoped_client_provider.test.js rename to src/core/server/saved_objects/service/lib/scoped_client_provider.test.js diff --git a/src/legacy/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/scoped_client_provider.ts rename to src/core/server/saved_objects/service/lib/scoped_client_provider.ts diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/__snapshots__/sorting_params.test.ts.snap b/src/core/server/saved_objects/service/lib/search_dsl/__snapshots__/sorting_params.test.ts.snap similarity index 100% rename from src/legacy/server/saved_objects/service/lib/search_dsl/__snapshots__/sorting_params.test.ts.snap rename to src/core/server/saved_objects/service/lib/search_dsl/__snapshots__/sorting_params.test.ts.snap diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/index.ts b/src/core/server/saved_objects/service/lib/search_dsl/index.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/search_dsl/index.ts rename to src/core/server/saved_objects/service/lib/search_dsl/index.ts diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/search_dsl/query_params.test.ts rename to src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts similarity index 99% rename from src/legacy/server/saved_objects/service/lib/search_dsl/query_params.ts rename to src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 8e1dc1ded3f2c..9c145258a755f 100644 --- a/src/legacy/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -17,7 +17,7 @@ * under the License. */ -import { getRootPropertiesObjects, IndexMapping } from '../../../../mappings'; +import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; /** diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts rename to src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts similarity index 97% rename from src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.ts rename to src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 83e06eb17ccf2..1c2c87bca6ea7 100644 --- a/src/legacy/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -19,7 +19,7 @@ import Boom from 'boom'; -import { IndexMapping } from '../../../../mappings'; +import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts similarity index 100% rename from src/legacy/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts rename to src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts diff --git a/src/legacy/server/saved_objects/service/lib/search_dsl/sorting_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts similarity index 96% rename from src/legacy/server/saved_objects/service/lib/search_dsl/sorting_params.ts rename to src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts index 444ab0774e7eb..d96d43eb9e7f6 100644 --- a/src/legacy/server/saved_objects/service/lib/search_dsl/sorting_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts @@ -18,7 +18,7 @@ */ import Boom from 'boom'; -import { getProperty, IndexMapping } from '../../../../mappings'; +import { getProperty, IndexMapping } from '../../../mappings'; const TOP_LEVEL_FIELDS = ['_id']; diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts similarity index 92% rename from src/legacy/server/saved_objects/service/saved_objects_client.mock.ts rename to src/core/server/saved_objects/service/saved_objects_client.mock.ts index 16de6e4eb5b52..4d1ceeaf552b6 100644 --- a/src/legacy/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -18,10 +18,10 @@ */ import { SavedObjectsClientContract } from './saved_objects_client'; -import * as errors from './lib/errors'; +import { SavedObjectsErrorHelpers } from './lib/errors'; const create = (): jest.Mocked => ({ - errors, + errors: SavedObjectsErrorHelpers, create: jest.fn(), bulkCreate: jest.fn(), delete: jest.fn(), diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js similarity index 100% rename from src/legacy/server/saved_objects/service/saved_objects_client.test.js rename to src/core/server/saved_objects/service/saved_objects_client.test.js diff --git a/src/legacy/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts similarity index 50% rename from src/legacy/server/saved_objects/service/saved_objects_client.ts rename to src/core/server/saved_objects/service/saved_objects_client.ts index a0d378bfc5a97..14a4f99314e03 100644 --- a/src/legacy/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -17,37 +17,59 @@ * under the License. */ -import { errors, SavedObjectsRepository } from './lib'; +import { SavedObjectsRepository } from './lib'; + +import { SavedObjectsErrorHelpers } from './lib/errors'; type Omit = Pick>; -export interface BaseOptions { +/** + * + * @public + */ +export interface SavedObjectsBaseOptions { /** Specify the namespace for this operation */ namespace?: string; } -export interface CreateOptions extends BaseOptions { +/** + * + * @public + */ +export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { /** (not recommended) Specify an id for the document */ id?: string; /** Overwrite existing documents (defaults to false) */ overwrite?: boolean; - migrationVersion?: MigrationVersion; + migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; } -export interface BulkCreateObject { +/** + * + * @public + */ +export interface SavedObjectsBulkCreateObject { id?: string; type: string; attributes: T; references?: SavedObjectReference[]; - migrationVersion?: MigrationVersion; + migrationVersion?: SavedObjectsMigrationVersion; } -export interface BulkResponse { +/** + * + * @public + */ +export interface SavedObjectsBulkResponse { saved_objects: Array>; } -export interface FindOptions extends BaseOptions { +/** + * + * @public + */ +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { type?: string | string[]; page?: number; perPage?: number; @@ -61,31 +83,51 @@ export interface FindOptions extends BaseOptions { defaultSearchOperator?: 'AND' | 'OR'; } -export interface FindResponse { +/** + * + * @public + */ +export interface SavedObjectsFindResponse { saved_objects: Array>; total: number; per_page: number; page: number; } -export interface UpdateOptions extends BaseOptions { +/** + * + * @public + */ +export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { /** Ensures version matches that of persisted object */ version?: string; references?: SavedObjectReference[]; } -export interface BulkGetObject { +/** + * + * @public + */ +export interface SavedObjectsBulkGetObject { id: string; type: string; /** SavedObject fields to include in the response */ fields?: string[]; } -export interface BulkResponse { +/** + * + * @public + */ +export interface SavedObjectsBulkResponse { saved_objects: Array>; } -export interface UpdateResponse +/** + * + * @public + */ +export interface SavedObjectsUpdateResponse extends Omit, 'attributes'> { attributes: Partial; } @@ -93,19 +135,25 @@ export interface UpdateResponse /** * A dictionary of saved object type -> version used to determine * what migrations need to be applied to a saved object. + * + * @public */ -export interface MigrationVersion { +export interface SavedObjectsMigrationVersion { [pluginName: string]: string; } +/** + * + * @public + */ export interface SavedObjectAttributes { [key: string]: SavedObjectAttributes | string | number | boolean | null; } -export interface VisualizationAttributes extends SavedObjectAttributes { - visState: string; -} - +/** + * + * @public + */ export interface SavedObject { id: string; type: string; @@ -117,11 +165,13 @@ export interface SavedObject { }; attributes: T; references: SavedObjectReference[]; - migrationVersion?: MigrationVersion; + migrationVersion?: SavedObjectsMigrationVersion; } /** * A reference to another saved object. + * + * @public */ export interface SavedObjectReference { name: string; @@ -129,76 +179,82 @@ export interface SavedObjectReference { id: string; } +/** + * ## SavedObjectsClient errors + * + * Since the SavedObjectsClient has its hands in everything we + * are a little paranoid about the way we present errors back to + * to application code. Ideally, all errors will be either: + * + * 1. Caused by bad implementation (ie. undefined is not a function) and + * as such unpredictable + * 2. An error that has been classified and decorated appropriately + * by the decorators in {@link SavedObjectsErrorHelpers} + * + * Type 1 errors are inevitable, but since all expected/handle-able errors + * should be Type 2 the `isXYZError()` helpers exposed at + * `SavedObjectsErrorHelpers` should be used to understand and manage error + * responses from the `SavedObjectsClient`. + * + * Type 2 errors are decorated versions of the source error, so if + * the elasticsearch client threw an error it will be decorated based + * on its type. That means that rather than looking for `error.body.error.type` or + * doing substring checks on `error.body.error.reason`, just use the helpers to + * understand the meaning of the error: + * + * ```js + * if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + * // handle 404 + * } + * + * if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { + * // 401 handling should be automatic, but in case you wanted to know + * } + * + * // always rethrow the error unless you handle it + * throw error; + * ``` + * + * ### 404s from missing index + * + * From the perspective of application code and APIs the SavedObjectsClient is + * a black box that persists objects. One of the internal details that users have + * no control over is that we use an elasticsearch index for persistance and that + * index might be missing. + * + * At the time of writing we are in the process of transitioning away from the + * operating assumption that the SavedObjects index is always available. Part of + * this transition is handling errors resulting from an index missing. These used + * to trigger a 500 error in most cases, and in others cause 404s with different + * error messages. + * + * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The + * object the request/call was targeting could not be found. This is why #14141 + * takes special care to ensure that 404 errors are generic and don't distinguish + * between index missing or document missing. + * + * ### 503s from missing index + * + * Unlike all other methods, create requests are supposed to succeed even when + * the Kibana index does not exist because it will be automatically created by + * elasticsearch. When that is not the case it is because Elasticsearch's + * `action.auto_create_index` setting prevents it from being created automatically + * so we throw a special 503 with the intention of informing the user that their + * Elasticsearch settings need to be updated. + * + * See {@link SavedObjectsErrorHelpers} + * + * @public + */ export type SavedObjectsClientContract = Pick; +/** + * + * @internal + */ export class SavedObjectsClient { - /** - * ## SavedObjectsClient errors - * - * Since the SavedObjectsClient has its hands in everything we - * are a little paranoid about the way we present errors back to - * to application code. Ideally, all errors will be either: - * - * 1. Caused by bad implementation (ie. undefined is not a function) and - * as such unpredictable - * 2. An error that has been classified and decorated appropriately - * by the decorators in `./lib/errors` - * - * Type 1 errors are inevitable, but since all expected/handle-able errors - * should be Type 2 the `isXYZError()` helpers exposed at - * `savedObjectsClient.errors` should be used to understand and manage error - * responses from the `SavedObjectsClient`. - * - * Type 2 errors are decorated versions of the source error, so if - * the elasticsearch client threw an error it will be decorated based - * on its type. That means that rather than looking for `error.body.error.type` or - * doing substring checks on `error.body.error.reason`, just use the helpers to - * understand the meaning of the error: - * - * ```js - * if (savedObjectsClient.errors.isNotFoundError(error)) { - * // handle 404 - * } - * - * if (savedObjectsClient.errors.isNotAuthorizedError(error)) { - * // 401 handling should be automatic, but in case you wanted to know - * } - * - * // always rethrow the error unless you handle it - * throw error; - * ``` - * - * ### 404s from missing index - * - * From the perspective of application code and APIs the SavedObjectsClient is - * a black box that persists objects. One of the internal details that users have - * no control over is that we use an elasticsearch index for persistance and that - * index might be missing. - * - * At the time of writing we are in the process of transitioning away from the - * operating assumption that the SavedObjects index is always available. Part of - * this transition is handling errors resulting from an index missing. These used - * to trigger a 500 error in most cases, and in others cause 404s with different - * error messages. - * - * From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The - * object the request/call was targeting could not be found. This is why #14141 - * takes special care to ensure that 404 errors are generic and don't distinguish - * between index missing or document missing. - * - * ### 503s from missing index - * - * Unlike all other methods, create requests are supposed to succeed even when - * the Kibana index does not exist because it will be automatically created by - * elasticsearch. When that is not the case it is because Elasticsearch's - * `action.auto_create_index` setting prevents it from being created automatically - * so we throw a special 503 with the intention of informing the user that their - * Elasticsearch settings need to be updated. - * - * @type {ErrorHelpers} see ./lib/errors - */ - public static errors = errors; - public errors = errors; + public static errors = SavedObjectsErrorHelpers; + public errors = SavedObjectsErrorHelpers; private _repository: SavedObjectsRepository; @@ -216,7 +272,7 @@ export class SavedObjectsClient { async create( type: string, attributes: T, - options?: CreateOptions + options?: SavedObjectsCreateOptions ) { return await this._repository.create(type, attributes, options); } @@ -228,8 +284,8 @@ export class SavedObjectsClient { * @param options */ async bulkCreate( - objects: Array>, - options?: CreateOptions + objects: Array>, + options?: SavedObjectsCreateOptions ) { return await this._repository.bulkCreate(objects, options); } @@ -241,7 +297,7 @@ export class SavedObjectsClient { * @param id * @param options */ - async delete(type: string, id: string, options: BaseOptions = {}) { + async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { return await this._repository.delete(type, id, options); } @@ -251,8 +307,8 @@ export class SavedObjectsClient { * @param options */ async find( - options: FindOptions - ): Promise> { + options: SavedObjectsFindOptions + ): Promise> { return await this._repository.find(options); } @@ -268,9 +324,9 @@ export class SavedObjectsClient { * ]) */ async bulkGet( - objects: BulkGetObject[] = [], - options: BaseOptions = {} - ): Promise> { + objects: SavedObjectsBulkGetObject[] = [], + options: SavedObjectsBaseOptions = {} + ): Promise> { return await this._repository.bulkGet(objects, options); } @@ -284,7 +340,7 @@ export class SavedObjectsClient { async get( type: string, id: string, - options: BaseOptions = {} + options: SavedObjectsBaseOptions = {} ): Promise> { return await this._repository.get(type, id, options); } @@ -300,8 +356,8 @@ export class SavedObjectsClient { type: string, id: string, attributes: Partial, - options: UpdateOptions = {} - ): Promise> { + options: SavedObjectsUpdateOptions = {} + ): Promise> { return await this._repository.update(type, id, attributes, options); } } diff --git a/src/legacy/server/saved_objects/validation/index.ts b/src/core/server/saved_objects/validation/index.ts similarity index 100% rename from src/legacy/server/saved_objects/validation/index.ts rename to src/core/server/saved_objects/validation/index.ts diff --git a/src/legacy/server/saved_objects/validation/readme.md b/src/core/server/saved_objects/validation/readme.md similarity index 100% rename from src/legacy/server/saved_objects/validation/readme.md rename to src/core/server/saved_objects/validation/readme.md diff --git a/src/legacy/server/saved_objects/validation/validation.test.ts b/src/core/server/saved_objects/validation/validation.test.ts similarity index 100% rename from src/legacy/server/saved_objects/validation/validation.test.ts rename to src/core/server/saved_objects/validation/validation.test.ts diff --git a/src/legacy/server/saved_objects/version/base64.ts b/src/core/server/saved_objects/version/base64.ts similarity index 100% rename from src/legacy/server/saved_objects/version/base64.ts rename to src/core/server/saved_objects/version/base64.ts diff --git a/src/legacy/server/saved_objects/version/decode_request_version.test.ts b/src/core/server/saved_objects/version/decode_request_version.test.ts similarity index 100% rename from src/legacy/server/saved_objects/version/decode_request_version.test.ts rename to src/core/server/saved_objects/version/decode_request_version.test.ts diff --git a/src/legacy/server/saved_objects/version/decode_request_version.ts b/src/core/server/saved_objects/version/decode_request_version.ts similarity index 100% rename from src/legacy/server/saved_objects/version/decode_request_version.ts rename to src/core/server/saved_objects/version/decode_request_version.ts diff --git a/src/legacy/server/saved_objects/version/decode_version.test.ts b/src/core/server/saved_objects/version/decode_version.test.ts similarity index 100% rename from src/legacy/server/saved_objects/version/decode_version.test.ts rename to src/core/server/saved_objects/version/decode_version.test.ts diff --git a/src/legacy/server/saved_objects/version/decode_version.ts b/src/core/server/saved_objects/version/decode_version.ts similarity index 91% rename from src/legacy/server/saved_objects/version/decode_version.ts rename to src/core/server/saved_objects/version/decode_version.ts index 92e06c080b087..73e8ca9639799 100644 --- a/src/legacy/server/saved_objects/version/decode_version.ts +++ b/src/core/server/saved_objects/version/decode_version.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createInvalidVersionError } from '../service/lib/errors'; +import { SavedObjectsErrorHelpers } from '../service/lib/errors'; import { decodeBase64 } from './base64'; /** @@ -46,6 +46,6 @@ export function decodeVersion(version?: string) { _primary_term: seqParams[1], }; } catch (_) { - throw createInvalidVersionError(version); + throw SavedObjectsErrorHelpers.createInvalidVersionError(version); } } diff --git a/src/legacy/server/saved_objects/version/encode_hit_version.test.ts b/src/core/server/saved_objects/version/encode_hit_version.test.ts similarity index 100% rename from src/legacy/server/saved_objects/version/encode_hit_version.test.ts rename to src/core/server/saved_objects/version/encode_hit_version.test.ts diff --git a/src/legacy/server/saved_objects/version/encode_hit_version.ts b/src/core/server/saved_objects/version/encode_hit_version.ts similarity index 100% rename from src/legacy/server/saved_objects/version/encode_hit_version.ts rename to src/core/server/saved_objects/version/encode_hit_version.ts diff --git a/src/legacy/server/saved_objects/version/encode_version.test.ts b/src/core/server/saved_objects/version/encode_version.test.ts similarity index 100% rename from src/legacy/server/saved_objects/version/encode_version.test.ts rename to src/core/server/saved_objects/version/encode_version.test.ts diff --git a/src/legacy/server/saved_objects/version/encode_version.ts b/src/core/server/saved_objects/version/encode_version.ts similarity index 100% rename from src/legacy/server/saved_objects/version/encode_version.ts rename to src/core/server/saved_objects/version/encode_version.ts diff --git a/src/legacy/server/saved_objects/version/index.ts b/src/core/server/saved_objects/version/index.ts similarity index 100% rename from src/legacy/server/saved_objects/version/index.ts rename to src/core/server/saved_objects/version/index.ts diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c0e1ce9028706..14dea167d3189 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -4,7 +4,9 @@ ```ts +import Boom from 'boom'; import { ByteSizeValue } from '@kbn/config-schema'; +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { ConfigOptions } from 'elasticsearch'; import { Duration } from 'moment'; import { ObjectType } from '@kbn/config-schema'; @@ -14,7 +16,6 @@ import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; import { Schema } from '@kbn/config-schema'; import { Server } from 'hapi'; -import { ServerOptions } from 'hapi'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Url } from 'url'; @@ -25,11 +26,20 @@ export type APICaller = (endpoint: string, clientParams: Record // Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export type AuthenticationHandler = (request: Readonly, t: AuthToolkit) => AuthResult | Promise; +export type AuthenticationHandler = (request: KibanaRequest, t: AuthToolkit) => AuthResult | Promise; + +// @public +export type AuthHeaders = Record; + +// @public +export interface AuthResultData { + headers: AuthHeaders; + state: Record; +} // @public export interface AuthToolkit { - authenticated: (state?: object) => AuthResult; + authenticated: (data?: Partial) => AuthResult; redirected: (url: string) => AuthResult; rejected: (error: Error, options?: { statusCode?: number; @@ -49,10 +59,8 @@ export interface CallAPIOptions { // @public export class ClusterClient { - constructor(config: ElasticsearchClientConfig, log: Logger); - asScoped(req?: { - headers?: Headers; - }): ScopedClusterClient; + constructor(config: ElasticsearchClientConfig, log: Logger, getAuthHeaders?: GetAuthHeaders); + asScoped(request?: KibanaRequest | LegacyRequest | FakeRequest): ScopedClusterClient; callAsInternalUser: (endpoint: string, clientParams?: Record, options?: CallAPIOptions | undefined) => Promise; close(): void; } @@ -128,6 +136,14 @@ export interface ElasticsearchServiceSetup { }; } +// @public +export interface FakeRequest { + headers: Record; +} + +// @public +export type GetAuthHeaders = (request: KibanaRequest | Request) => AuthHeaders | undefined; + // @public (undocumented) export type Headers = Record; @@ -143,7 +159,7 @@ export interface HttpServiceSetup extends HttpServerSetup { // @public (undocumented) export interface HttpServiceStart { - isListening: () => boolean; + isListening: (port: number) => boolean; } // @internal (undocumented) @@ -158,8 +174,6 @@ export interface InternalCoreSetup { // @public (undocumented) export interface InternalCoreStart { - // (undocumented) - http: HttpServiceStart; // (undocumented) plugins: PluginsServiceStart; } @@ -168,16 +182,15 @@ export interface InternalCoreStart { export class KibanaRequest { // @internal (undocumented) protected readonly [requestSymbol]: Request; - constructor(request: Request, params: Params, query: Query, body: Body); + constructor(request: Request, params: Params, query: Query, body: Body, withoutSecretHeaders: boolean); // (undocumented) readonly body: Body; // Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts // // @internal - static from

(req: Request, routeSchemas?: RouteSchemas): KibanaRequest; + static from

(req: Request, routeSchemas?: RouteSchemas, withoutSecretHeaders?: boolean): KibanaRequest; // (undocumented) getFilteredHeaders(headersToKeep: string[]): Pick, string>; - // (undocumented) readonly headers: Headers; // (undocumented) readonly params: Params; @@ -199,6 +212,9 @@ export interface KibanaRequestRoute { path: string; } +// @public +export type LegacyRequest = Request; + // @public export interface Logger { debug(message: string, meta?: LogMeta): void; @@ -380,6 +396,259 @@ export class Router { routes: Array>; } +// @public (undocumented) +export interface SavedObject { + // (undocumented) + attributes: T; + // (undocumented) + error?: { + message: string; + statusCode: number; + }; + // (undocumented) + id: string; + // (undocumented) + migrationVersion?: SavedObjectsMigrationVersion; + // (undocumented) + references: SavedObjectReference[]; + // (undocumented) + type: string; + // (undocumented) + updated_at?: string; + // (undocumented) + version?: string; +} + +// @public (undocumented) +export interface SavedObjectAttributes { + // (undocumented) + [key: string]: SavedObjectAttributes | string | number | boolean | null; +} + +// @public +export interface SavedObjectReference { + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBaseOptions { + namespace?: string; +} + +// @public (undocumented) +export interface SavedObjectsBulkCreateObject { + // (undocumented) + attributes: T; + // (undocumented) + id?: string; + // (undocumented) + migrationVersion?: SavedObjectsMigrationVersion; + // (undocumented) + references?: SavedObjectReference[]; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBulkGetObject { + fields?: string[]; + // (undocumented) + id: string; + // (undocumented) + type: string; +} + +// @public (undocumented) +export interface SavedObjectsBulkResponse { + // (undocumented) + saved_objects: Array>; +} + +// @public (undocumented) +export interface SavedObjectsBulkResponse { + // (undocumented) + saved_objects: Array>; +} + +// @internal (undocumented) +export class SavedObjectsClient { + // Warning: (ae-forgotten-export) The symbol "SavedObjectsRepository" needs to be exported by the entry point index.d.ts + constructor(repository: SavedObjectsRepository); + bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; + bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; + create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; + delete(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<{}>; + // (undocumented) + errors: typeof SavedObjectsErrorHelpers; + // (undocumented) + static errors: typeof SavedObjectsErrorHelpers; + find(options: SavedObjectsFindOptions): Promise>; + get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; + update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; +} + +// Warning: (ae-incompatible-release-tags) The symbol "SavedObjectsClientContract" is marked as @public, but its signature references "SavedObjectsClient" which is marked as @internal +// +// @public +export type SavedObjectsClientContract = Pick; + +// Warning: (ae-missing-release-tag) "SavedObjectsClientWrapperFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SavedObjectsClientWrapperFactory = (options: SavedObjectsClientWrapperOptions) => SavedObjectsClientContract; + +// Warning: (ae-missing-release-tag) "SavedObjectsClientWrapperOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface SavedObjectsClientWrapperOptions { + // (undocumented) + client: SavedObjectsClientContract; + // (undocumented) + request: Request; +} + +// @public (undocumented) +export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { + id?: string; + // (undocumented) + migrationVersion?: SavedObjectsMigrationVersion; + overwrite?: boolean; + // (undocumented) + references?: SavedObjectReference[]; +} + +// @public (undocumented) +export class SavedObjectsErrorHelpers { + // (undocumented) + static createBadRequestError(reason?: string): DecoratedError; + // (undocumented) + static createEsAutoCreateIndexError(): DecoratedError; + // (undocumented) + static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; + // (undocumented) + static createInvalidVersionError(versionInput?: string): DecoratedError; + // (undocumented) + static createUnsupportedTypeError(type: string): DecoratedError; + // (undocumented) + static decorateBadRequestError(error: Error, reason?: string): DecoratedError; + // (undocumented) + static decorateConflictError(error: Error, reason?: string): DecoratedError; + // (undocumented) + static decorateEsUnavailableError(error: Error, reason?: string): DecoratedError; + // (undocumented) + static decorateForbiddenError(error: Error, reason?: string): DecoratedError; + // (undocumented) + static decorateGeneralError(error: Error, reason?: string): DecoratedError; + // (undocumented) + static decorateNotAuthorizedError(error: Error, reason?: string): DecoratedError; + // (undocumented) + static decorateRequestEntityTooLargeError(error: Error, reason?: string): DecoratedError; + // (undocumented) + static isBadRequestError(error: Error | DecoratedError): boolean; + // (undocumented) + static isConflictError(error: Error | DecoratedError): boolean; + // (undocumented) + static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; + // (undocumented) + static isEsUnavailableError(error: Error | DecoratedError): boolean; + // (undocumented) + static isForbiddenError(error: Error | DecoratedError): boolean; + // (undocumented) + static isInvalidVersionError(error: Error | DecoratedError): boolean; + // (undocumented) + static isNotAuthorizedError(error: Error | DecoratedError): boolean; + // (undocumented) + static isNotFoundError(error: Error | DecoratedError): boolean; + // (undocumented) + static isRequestEntityTooLargeError(error: Error | DecoratedError): boolean; + // Warning: (ae-forgotten-export) The symbol "DecoratedError" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static isSavedObjectsClientError(error: any): error is DecoratedError; +} + +// @public (undocumented) +export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { + // (undocumented) + defaultSearchOperator?: 'AND' | 'OR'; + // (undocumented) + fields?: string[]; + // (undocumented) + hasReference?: { + type: string; + id: string; + }; + // (undocumented) + page?: number; + // (undocumented) + perPage?: number; + // (undocumented) + search?: string; + searchFields?: string[]; + // (undocumented) + sortField?: string; + // (undocumented) + sortOrder?: string; + // (undocumented) + type?: string | string[]; +} + +// @public (undocumented) +export interface SavedObjectsFindResponse { + // (undocumented) + page: number; + // (undocumented) + per_page: number; + // (undocumented) + saved_objects: Array>; + // (undocumented) + total: number; +} + +// @public +export interface SavedObjectsMigrationVersion { + // (undocumented) + [pluginName: string]: string; +} + +// @public (undocumented) +export interface SavedObjectsService { + // Warning: (ae-forgotten-export) The symbol "ScopedSavedObjectsClientProvider" needs to be exported by the entry point index.d.ts + // + // (undocumented) + addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider['addClientWrapperFactory']; + // (undocumented) + getSavedObjectsRepository(...rest: any[]): any; + // (undocumented) + getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; + // Warning: (ae-incompatible-release-tags) The symbol "SavedObjectsClient" is marked as @public, but its signature references "SavedObjectsClient" which is marked as @internal + // + // (undocumented) + SavedObjectsClient: typeof SavedObjectsClient; + // (undocumented) + types: string[]; +} + +// @public (undocumented) +export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { + // (undocumented) + references?: SavedObjectReference[]; + version?: string; +} + +// Warning: (ae-forgotten-export) The symbol "Omit" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export interface SavedObjectsUpdateResponse extends Omit, 'attributes'> { + // (undocumented) + attributes: Partial; +} + // @public export class ScopedClusterClient { constructor(internalAPICaller: APICaller, scopedAPICaller: APICaller, headers?: Record | undefined); @@ -397,7 +666,7 @@ export interface SessionStorage { // @public export interface SessionStorageFactory { // (undocumented) - asScoped: (request: Readonly | KibanaRequest) => SessionStorage; + asScoped: (request: KibanaRequest) => SessionStorage; } diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 257b9e72fd081..694888ab6243e 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -18,11 +18,11 @@ */ import { - elasticsearchService, - httpService, + mockElasticsearchService, + mockHttpService, mockLegacyService, mockPluginsService, - configService, + mockConfigService, } from './index.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -36,7 +36,7 @@ const env = new Env('.', getEnvOptions()); const logger = loggingServiceMock.create(); beforeEach(() => { - configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); + mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); }); afterEach(() => { @@ -47,15 +47,15 @@ const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); test('sets up services on "setup"', async () => { const server = new Server(config$, env, logger); - expect(httpService.setup).not.toHaveBeenCalled(); - expect(elasticsearchService.setup).not.toHaveBeenCalled(); + expect(mockHttpService.setup).not.toHaveBeenCalled(); + expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); await server.setup(); - expect(httpService.setup).toHaveBeenCalledTimes(1); - expect(elasticsearchService.setup).toHaveBeenCalledTimes(1); + expect(mockHttpService.setup).toHaveBeenCalledTimes(1); + expect(mockElasticsearchService.setup).toHaveBeenCalledTimes(1); expect(mockPluginsService.setup).toHaveBeenCalledTimes(1); expect(mockLegacyService.setup).toHaveBeenCalledTimes(1); }); @@ -63,21 +63,21 @@ test('sets up services on "setup"', async () => { test('runs services on "start"', async () => { const server = new Server(config$, env, logger); - expect(httpService.setup).not.toHaveBeenCalled(); + expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.start).not.toHaveBeenCalled(); await server.setup(); - expect(httpService.start).not.toHaveBeenCalled(); + expect(mockHttpService.start).not.toHaveBeenCalled(); expect(mockLegacyService.start).not.toHaveBeenCalled(); await server.start(); - expect(httpService.start).toHaveBeenCalledTimes(1); + expect(mockHttpService.start).toHaveBeenCalledTimes(1); expect(mockLegacyService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { - configService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']); + mockConfigService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']); const server = new Server(config$, env, logger); @@ -89,29 +89,29 @@ test('stops services on "stop"', async () => { await server.setup(); - expect(httpService.stop).not.toHaveBeenCalled(); - expect(elasticsearchService.stop).not.toHaveBeenCalled(); + expect(mockHttpService.stop).not.toHaveBeenCalled(); + expect(mockElasticsearchService.stop).not.toHaveBeenCalled(); expect(mockPluginsService.stop).not.toHaveBeenCalled(); expect(mockLegacyService.stop).not.toHaveBeenCalled(); await server.stop(); - expect(httpService.stop).toHaveBeenCalledTimes(1); - expect(elasticsearchService.stop).toHaveBeenCalledTimes(1); + expect(mockHttpService.stop).toHaveBeenCalledTimes(1); + expect(mockElasticsearchService.stop).toHaveBeenCalledTimes(1); expect(mockPluginsService.stop).toHaveBeenCalledTimes(1); expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { - configService.setSchema.mockImplementation(() => { + mockConfigService.setSchema.mockImplementation(() => { throw new Error('invalid config'); }); const server = new Server(config$, env, logger); await expect(server.setupConfigSchemas()).rejects.toThrowErrorMatchingInlineSnapshot( `"invalid config"` ); - expect(httpService.setup).not.toHaveBeenCalled(); - expect(elasticsearchService.setup).not.toHaveBeenCalled(); + expect(mockHttpService.setup).not.toHaveBeenCalled(); + expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 4f56e20f2c021..01f2673c3f9e5 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -61,7 +61,9 @@ export class Server { const httpSetup = await this.http.setup(); this.registerDefaultRoute(httpSetup); - const elasticsearchServiceSetup = await this.elasticsearch.setup(); + const elasticsearchServiceSetup = await this.elasticsearch.setup({ + http: httpSetup, + }); const pluginsSetup = await this.plugins.setup({ elasticsearch: elasticsearchServiceSetup, @@ -83,11 +85,9 @@ export class Server { } public async start() { - const httpStart = await this.http.start(); const pluginsStart = await this.plugins.start({}); const coreStart = { - http: httpStart, plugins: pluginsStart, }; @@ -96,6 +96,7 @@ export class Server { plugins: mapToObject(pluginsStart.contracts), }); + await this.http.start(); return coreStart; } diff --git a/src/core/utils/map_to_object.ts b/src/core/utils/map_to_object.ts index 14767f566674f..bfbe5c8ab0bea 100644 --- a/src/core/utils/map_to_object.ts +++ b/src/core/utils/map_to_object.ts @@ -17,8 +17,8 @@ * under the License. */ -export function mapToObject(map: Map) { - const result: Record = Object.create(null); +export function mapToObject(map: Map) { + const result: Record = Object.create(null); for (const [key, value] of map) { result[key] = value; } diff --git a/src/dev/build/tasks/create_archives_task.js b/src/dev/build/tasks/create_archives_task.js index cc4365dc1eb75..80ef212a65f23 100644 --- a/src/dev/build/tasks/create_archives_task.js +++ b/src/dev/build/tasks/create_archives_task.js @@ -24,7 +24,8 @@ export const CreateArchivesTask = { description: 'Creating the archives for each platform', async run(config, log, build) { - await Promise.all(config.getTargetPlatforms().map(async platform => { + // archive one at a time, parallel causes OOM sometimes + for (const platform of config.getTargetPlatforms()) { const source = build.resolvePathForPlatform(platform, '.'); const destination = build.getPlatformArchivePath(platform); @@ -69,6 +70,6 @@ export const CreateArchivesTask = { default: throw new Error(`Unexpected extension for archive destination: ${destination}`); } - })); + } } }; diff --git a/src/dev/build/tasks/create_empty_dirs_and_files_task.js b/src/dev/build/tasks/create_empty_dirs_and_files_task.js index 7badb1c498902..e9561a067613f 100644 --- a/src/dev/build/tasks/create_empty_dirs_and_files_task.js +++ b/src/dev/build/tasks/create_empty_dirs_and_files_task.js @@ -26,7 +26,7 @@ export const CreateEmptyDirsAndFilesTask = { await Promise.all([ mkdirp(build.resolvePath('plugins')), mkdirp(build.resolvePath('data')), - write(build.resolvePath('optimize/.babel_register_cache.json'), '{}'), + write(build.resolvePath('optimize/.babelcache.json'), '{}'), ]); }, }; diff --git a/src/dev/build/tasks/optimize_task.js b/src/dev/build/tasks/optimize_task.js index f6de0c717abfe..1b71c5e1f5190 100644 --- a/src/dev/build/tasks/optimize_task.js +++ b/src/dev/build/tasks/optimize_task.js @@ -48,7 +48,6 @@ export const OptimizeBuildTask = { cwd: build.resolvePath('.'), env: { FORCE_DLL_CREATION: 'true', - KBN_CACHE_LOADER_WRITABLE: 'true', NODE_OPTIONS: '--max-old-space-size=2048' }, }); diff --git a/src/dev/failed_tests/__fixtures__/mocha_report.xml b/src/dev/failed_tests/__fixtures__/mocha_report.xml new file mode 100644 index 0000000000000..7a89b3a6ecabe --- /dev/null +++ b/src/dev/failed_tests/__fixtures__/mocha_report.xml @@ -0,0 +1,38 @@ + + + + + + + + + +503 Service Temporarily Unavailable + +

503 Service Temporarily Unavailable

+
nginx/1.13.7
+ + + + at Function.getSnapshot (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/packages/kbn-es/src/artifact.js:95:13) + at process._tickCallback (internal/process/next_tick.js:68:7)]]> + + + + + + + + + + + + + + + + + diff --git a/src/dev/failed_tests/report.js b/src/dev/failed_tests/report.js index 60c3b78c80e89..1863c6d4e10e1 100644 --- a/src/dev/failed_tests/report.js +++ b/src/dev/failed_tests/report.js @@ -33,7 +33,7 @@ const indent = text => ( ` ${text.split('\n').map(l => ` ${l}`).join('\n')}` ); -const isLikelyIrrelevant = ({ failure }) => { +const isLikelyIrrelevant = ({ name, failure }) => { if (failure.includes('NoSuchSessionError: This driver instance does not have a valid session ID')) { return true; } @@ -42,6 +42,14 @@ const isLikelyIrrelevant = ({ failure }) => { return true; } + if (name.includes('"after all" hook') && failure.includes(`Cannot read property 'shutdown' of undefined`)) { + return true; + } + + if (failure.includes('Unable to read artifact info') && failure.includes('Service Temporarily Unavailable')) { + return true; + } + if (failure.includes('Unable to fetch Kibana status API response from Kibana')) { return true; } diff --git a/src/dev/failed_tests/report.test.js b/src/dev/failed_tests/report.test.js index f6cc8d4550720..693f3e8cd614c 100644 --- a/src/dev/failed_tests/report.test.js +++ b/src/dev/failed_tests/report.test.js @@ -107,4 +107,48 @@ Wait timed out after 10055ms `); }); }); + + describe('mocha report', () => { + it('allows relevant tests', async () => { + const failures = await createPromiseFromStreams([ + vfs.src([resolve(__dirname, '__fixtures__/mocha_report.xml')]), + mapXml(), + filterFailures(), + createConcatStream(), + ]); + + expect(console.log.mock.calls).toMatchInlineSnapshot(` +Array [ + Array [ + "Ignoring likely irrelevant failure: X-Pack Mocha Tests.x-pack/plugins/code/server/__tests__/multi_node·ts - code in multiple nodes \\"before all\\" hook + + Error: Unable to read artifact info from https://artifacts-api.elastic.co/v1/versions/8.0.0-SNAPSHOT/builds/latest/projects/elasticsearch: Service Temporarily Unavailable + + 503 Service Temporarily Unavailable + +

503 Service Temporarily Unavailable

+
nginx/1.13.7
+ + + + at Function.getSnapshot (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/packages/kbn-es/src/artifact.js:95:13) + at process._tickCallback (internal/process/next_tick.js:68:7) + ", + ], + Array [ + "Ignoring likely irrelevant failure: X-Pack Mocha Tests.x-pack/plugins/code/server/__tests__/multi_node·ts - code in multiple nodes \\"after all\\" hook + + TypeError: Cannot read property 'shutdown' of undefined + at Context.shutdown (plugins/code/server/__tests__/multi_node.ts:125:23) + at process.topLevelDomainCallback (domain.js:120:23) + ", + ], + Array [ + "Found 0 test failures", + ], +] +`); + expect(failures).toMatchInlineSnapshot(`Array []`); + }); + }); }); diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 9a992d7406ef4..a449d19bc6e87 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -62,11 +62,20 @@ See .i18nrc.json for the list of supported namespaces.`) } } -export async function extractMessagesFromPathToMap(inputPath, targetMap, config, reporter) { +export async function matchEntriesWithExctractors(inputPath, options = {}) { + const { + additionalIgnore = [], + mark = false, + absolute = false, + } = options; + const ignore = ['**/node_modules/**', '**/__tests__/**', '**/*.test.{js,jsx,ts,tsx}', '**/*.d.ts'].concat(additionalIgnore); + const entries = await globAsync('*.{js,jsx,pug,ts,tsx,html}', { cwd: inputPath, matchBase: true, - ignore: ['**/node_modules/**', '**/__tests__/**', '**/*.test.{js,jsx,ts,tsx}', '**/*.d.ts'], + ignore, + mark, + absolute, }); const { htmlEntries, codeEntries, pugEntries } = entries.reduce( @@ -86,37 +95,43 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap, config, { htmlEntries: [], codeEntries: [], pugEntries: [] } ); - await Promise.all( - [ - [htmlEntries, extractHtmlMessages], - [codeEntries, extractCodeMessages], - [pugEntries, extractPugMessages], - ].map(async ([entries, extractFunction]) => { - const files = await Promise.all( - filterEntries(entries, config.exclude).map(async entry => { - return { - name: entry, - content: await readFileAsync(entry), - }; - }) - ); - - for (const { name, content } of files) { - const reporterWithContext = reporter.withContext({ name }); - - try { - for (const [id, value] of extractFunction(content, reporterWithContext)) { - validateMessageNamespace(id, name, config.paths, reporterWithContext); - addMessageToMap(targetMap, id, value, reporterWithContext); - } - } catch (error) { - if (!isFailError(error)) { - throw error; - } + return [ + [htmlEntries, extractHtmlMessages], + [codeEntries, extractCodeMessages], + [pugEntries, extractPugMessages], + ]; +} - reporterWithContext.report(error); +export async function extractMessagesFromPathToMap(inputPath, targetMap, config, reporter) { + const categorizedEntries = await matchEntriesWithExctractors(inputPath); + return Promise.all( + categorizedEntries + .map(async ([entries, extractFunction]) => { + const files = await Promise.all( + filterEntries(entries, config.exclude).map(async entry => { + return { + name: entry, + content: await readFileAsync(entry), + }; + }) + ); + + for (const { name, content } of files) { + const reporterWithContext = reporter.withContext({ name }); + + try { + for (const [id, value] of extractFunction(content, reporterWithContext)) { + validateMessageNamespace(id, name, config.paths, reporterWithContext); + addMessageToMap(targetMap, id, value, reporterWithContext); + } + } catch (error) { + if (!isFailError(error)) { + throw error; + } + + reporterWithContext.report(error); + } } - } - }) + }) ); } diff --git a/src/dev/i18n/index.ts b/src/dev/i18n/index.ts index 604d7bab72c92..8f3e595509739 100644 --- a/src/dev/i18n/index.ts +++ b/src/dev/i18n/index.ts @@ -20,6 +20,8 @@ // @ts-ignore export { extractMessagesFromPathToMap } from './extract_default_translations'; // @ts-ignore +export { matchEntriesWithExctractors } from './extract_default_translations'; +// @ts-ignore export { writeFileAsync, readFileAsync, normalizePath, ErrorReporter } from './utils'; export { serializeToJson, serializeToJson5 } from './serializers'; export { I18nConfig, filterConfigPaths, mergeConfigs } from './config'; diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts index b090eedce443d..7ed5e788be298 100644 --- a/src/dev/i18n/integrate_locale_files.ts +++ b/src/dev/i18n/integrate_locale_files.ts @@ -37,7 +37,7 @@ import { createFailError } from '../run'; import { I18nConfig } from './config'; import { serializeToJson } from './serializers'; -interface IntegrateOptions { +export interface IntegrateOptions { sourceFileName: string; targetFileName?: string; dryRun: boolean; diff --git a/src/dev/i18n/tasks/check_compatibility.ts b/src/dev/i18n/tasks/check_compatibility.ts new file mode 100644 index 0000000000000..3c5f8c23466a4 --- /dev/null +++ b/src/dev/i18n/tasks/check_compatibility.ts @@ -0,0 +1,48 @@ +/* + * 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 { ToolingLog } from '@kbn/dev-utils'; +import { integrateLocaleFiles, I18nConfig } from '..'; + +export interface I18nFlags { + fix: boolean; + ignoreIncompatible: boolean; + ignoreUnused: boolean; + ignoreMissing: boolean; +} + +export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: ToolingLog) { + const { fix, ignoreIncompatible, ignoreUnused, ignoreMissing } = flags; + return config.translations.map(translationsPath => ({ + task: async ({ messages }: { messages: Map }) => { + // If `fix` is set we should try apply all possible fixes and override translations file. + await integrateLocaleFiles(messages, { + dryRun: !fix, + ignoreIncompatible: fix || ignoreIncompatible, + ignoreUnused: fix || ignoreUnused, + ignoreMissing: fix || ignoreMissing, + sourceFileName: translationsPath, + targetFileName: fix ? translationsPath : undefined, + config, + log, + }); + }, + title: `Compatibility check with ${translationsPath}`, + })); +} diff --git a/src/dev/i18n/tasks/extract_default_translations.ts b/src/dev/i18n/tasks/extract_default_translations.ts index 02e45350e249a..92bf9663e975e 100644 --- a/src/dev/i18n/tasks/extract_default_translations.ts +++ b/src/dev/i18n/tasks/extract_default_translations.ts @@ -18,19 +18,18 @@ */ import chalk from 'chalk'; -import Listr from 'listr'; - import { ErrorReporter, extractMessagesFromPathToMap, filterConfigPaths, I18nConfig } from '..'; import { createFailError } from '../../run'; -export async function extractDefaultMessages({ +export function extractDefaultMessages({ path, config, }: { path?: string | string[]; config: I18nConfig; }) { - const filteredPaths = filterConfigPaths(Array.isArray(path) ? path : [path || './'], config); + const inputPaths = Array.isArray(path) ? path : [path || './']; + const filteredPaths = filterConfigPaths(inputPaths, config) as string[]; if (filteredPaths.length === 0) { throw createFailError( `${chalk.white.bgRed( @@ -39,36 +38,22 @@ export async function extractDefaultMessages({ ); } - const reporter = new ErrorReporter(); - - const list = new Listr( - filteredPaths.map(filteredPath => ({ - task: async (messages: Map) => { - const initialErrorsNumber = reporter.errors.length; - - // Return result if no new errors were reported for this path. - const result = await extractMessagesFromPathToMap(filteredPath, messages, config, reporter); - if (reporter.errors.length === initialErrorsNumber) { - return result; - } - - // Throw an empty error to make Listr mark the task as failed without any message. - throw new Error(''); - }, - title: filteredPath, - })), - { - exitOnError: false, - } - ); - - try { - return await list.run(new Map()); - } catch (error) { - if (error.name === 'ListrError' && reporter.errors.length) { - throw createFailError(reporter.errors.join('\n\n')); - } - - throw error; - } + return filteredPaths.map(filteredPath => ({ + task: async (context: { + messages: Map; + reporter: ErrorReporter; + }) => { + const { messages, reporter } = context; + const initialErrorsNumber = reporter.errors.length; + + // Return result if no new errors were reported for this path. + const result = await extractMessagesFromPathToMap(filteredPath, messages, config, reporter); + if (reporter.errors.length === initialErrorsNumber) { + return result; + } + + throw reporter; + }, + title: filteredPath, + })); } diff --git a/src/dev/i18n/tasks/extract_untracked_translations.ts b/src/dev/i18n/tasks/extract_untracked_translations.ts new file mode 100644 index 0000000000000..cf83f02b76d82 --- /dev/null +++ b/src/dev/i18n/tasks/extract_untracked_translations.ts @@ -0,0 +1,109 @@ +/* + * 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 { + I18nConfig, + matchEntriesWithExctractors, + normalizePath, + readFileAsync, + ErrorReporter, +} from '..'; +import { createFailError } from '../../run'; + +function filterEntries(entries: string[], exclude: string[]) { + return entries.filter((entry: string) => + exclude.every((excludedPath: string) => !normalizePath(entry).startsWith(excludedPath)) + ); +} + +export async function extractUntrackedMessagesTask({ + path, + config, + reporter, +}: { + path?: string | string[]; + config: I18nConfig; + reporter: any; +}) { + const inputPaths = Array.isArray(path) ? path : [path || './']; + const availablePaths = Object.values(config.paths); + const ignore = availablePaths.concat([ + '**/build/**', + '**/webpackShims/**', + '**/__fixtures__/**', + '**/packages/kbn-i18n/**', + '**/packages/kbn-plugin-generator/sao_template/**', + '**/packages/kbn-ui-framework/generator-kui/**', + '**/target/**', + '**/test/**', + '**/scripts/**', + '**/src/dev/**', + '**/target/**', + '**/dist/**', + ]); + for (const inputPath of inputPaths) { + const categorizedEntries = await matchEntriesWithExctractors(inputPath, { + additionalIgnore: ignore, + mark: true, + absolute: true, + }); + + for (const [entries, extractFunction] of categorizedEntries) { + const files = await Promise.all( + filterEntries(entries, config.exclude) + .filter(entry => { + const normalizedEntry = normalizePath(entry); + return !availablePaths.some( + availablePath => + normalizedEntry.startsWith(`${normalizePath(availablePath)}/`) || + normalizePath(availablePath) === normalizedEntry + ); + }) + .map(async (entry: any) => ({ + name: entry, + content: await readFileAsync(entry), + })) + ); + + for (const { name, content } of files) { + const reporterWithContext = reporter.withContext({ name }); + for (const [id] of extractFunction(content, reporterWithContext)) { + const errorMessage = `Untracked file contains i18n label (${id}).`; + reporterWithContext.report(createFailError(errorMessage)); + } + } + } + } +} + +export function extractUntrackedMessages(srcPaths: string[], config: I18nConfig) { + return srcPaths.map(srcPath => ({ + title: `Checking untracked messages in ${srcPath}`, + task: async (context: { reporter: ErrorReporter }) => { + const { reporter } = context; + const initialErrorsNumber = reporter.errors.length; + const result = await extractUntrackedMessagesTask({ path: srcPath, config, reporter }); + if (reporter.errors.length === initialErrorsNumber) { + return result; + } + + throw reporter; + }, + })); +} diff --git a/src/dev/i18n/tasks/index.ts b/src/dev/i18n/tasks/index.ts index 00b3466d59276..bd33baa989e0f 100644 --- a/src/dev/i18n/tasks/index.ts +++ b/src/dev/i18n/tasks/index.ts @@ -18,3 +18,5 @@ */ export { extractDefaultMessages } from './extract_default_translations'; +export { extractUntrackedMessages } from './extract_untracked_translations'; +export { checkCompatibility } from './check_compatibility'; diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts index cb8fc0ca7902b..aad5da08df4ad 100644 --- a/src/dev/run_i18n_check.ts +++ b/src/dev/run_i18n_check.ts @@ -20,8 +20,8 @@ import chalk from 'chalk'; import Listr from 'listr'; -import { integrateLocaleFiles, mergeConfigs } from './i18n'; -import { extractDefaultMessages } from './i18n/tasks'; +import { ErrorReporter, mergeConfigs } from './i18n'; +import { extractDefaultMessages, extractUntrackedMessages, checkCompatibility } from './i18n/tasks'; import { createFailError, run } from './run'; run( @@ -60,48 +60,58 @@ run( } const config = await mergeConfigs(includeConfig); - const defaultMessages = await extractDefaultMessages({ path, config }); + const srcPaths = Array().concat(path || ['./src', './packages', './x-pack']); if (config.translations.length === 0) { return; } const list = new Listr( - config.translations.map(translationsPath => ({ - task: async () => { - // If `--fix` is set we should try apply all possible fixes and override translations file. - await integrateLocaleFiles(defaultMessages, { - sourceFileName: translationsPath, - targetFileName: fix ? translationsPath : undefined, - dryRun: !fix, - ignoreIncompatible: fix || !!ignoreIncompatible, - ignoreUnused: fix || !!ignoreUnused, - ignoreMissing: fix || !!ignoreMissing, - config, - log, - }); + [ + { + title: 'Checking For Untracked Messages', + task: () => new Listr(extractUntrackedMessages(srcPaths, config), { exitOnError: true }), }, - title: `Compatibility check with ${translationsPath}`, - })), + { + title: 'Validating Default Messages', + task: () => + new Listr(extractDefaultMessages({ path: srcPaths, config }), { exitOnError: true }), + }, + { + title: 'Compatibility Checks', + task: () => + new Listr( + checkCompatibility( + config, + { + ignoreIncompatible: !!ignoreIncompatible, + ignoreUnused: !!ignoreUnused, + ignoreMissing: !!ignoreMissing, + fix, + }, + log + ), + { exitOnError: true } + ), + }, + ], { - concurrent: true, - exitOnError: false, + concurrent: false, + exitOnError: true, } ); try { - await list.run(); + const reporter = new ErrorReporter(); + const messages: Map = new Map(); + await list.run({ messages, reporter }); } catch (error) { process.exitCode = 1; - - if (!error.errors) { + if (error instanceof ErrorReporter) { + error.errors.forEach((e: string | Error) => log.error(e)); + } else { log.error('Unhandled exception!'); log.error(error); - process.exit(); - } - - for (const e of error.errors) { - log.error(e); } } }, diff --git a/src/dev/run_i18n_extract.ts b/src/dev/run_i18n_extract.ts index 6670e81949609..63f7429ec420e 100644 --- a/src/dev/run_i18n_extract.ts +++ b/src/dev/run_i18n_extract.ts @@ -18,9 +18,16 @@ */ import chalk from 'chalk'; +import Listr from 'listr'; import { resolve } from 'path'; -import { mergeConfigs, serializeToJson, serializeToJson5, writeFileAsync } from './i18n'; +import { + ErrorReporter, + mergeConfigs, + serializeToJson, + serializeToJson5, + writeFileAsync, +} from './i18n'; import { extractDefaultMessages } from './i18n/tasks'; import { createFailError, run } from './run'; @@ -32,6 +39,7 @@ run( 'output-format': outputFormat, 'include-config': includeConfig, }, + log, }) => { if (!outputDir || typeof outputDir !== 'string') { throw createFailError( @@ -46,18 +54,42 @@ run( } const config = await mergeConfigs(includeConfig); - const defaultMessages = await extractDefaultMessages({ path, config }); - // Messages shouldn't be written to a file if output is not supplied. - if (!outputDir || !defaultMessages.size) { - return; - } + const list = new Listr([ + { + title: 'Extracting Default Messages', + task: () => new Listr(extractDefaultMessages({ path, config }), { exitOnError: true }), + }, + { + title: 'Writing to file', + enabled: ctx => outputDir && ctx.messages.size, + task: async ctx => { + const sortedMessages = [...ctx.messages].sort(([key1], [key2]) => + key1.localeCompare(key2) + ); + await writeFileAsync( + resolve(outputDir, 'en.json'), + outputFormat === 'json5' + ? serializeToJson5(sortedMessages) + : serializeToJson(sortedMessages) + ); + }, + }, + ]); - const sortedMessages = [...defaultMessages].sort(([key1], [key2]) => key1.localeCompare(key2)); - await writeFileAsync( - resolve(outputDir, 'en.json'), - outputFormat === 'json5' ? serializeToJson5(sortedMessages) : serializeToJson(sortedMessages) - ); + try { + const reporter = new ErrorReporter(); + const messages: Map = new Map(); + await list.run({ messages, reporter }); + } catch (error) { + process.exitCode = 1; + if (error instanceof ErrorReporter) { + error.errors.forEach((e: string | Error) => log.error(e)); + } else { + log.error('Unhandled exception!'); + log.error(error); + } + } }, { flags: { diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts index e5cf21fb88ba3..b80dd8deac274 100644 --- a/src/dev/run_i18n_integrate.ts +++ b/src/dev/run_i18n_integrate.ts @@ -18,8 +18,9 @@ */ import chalk from 'chalk'; +import Listr from 'listr'; -import { integrateLocaleFiles, mergeConfigs } from './i18n'; +import { ErrorReporter, integrateLocaleFiles, mergeConfigs } from './i18n'; import { extractDefaultMessages } from './i18n/tasks'; import { createFailError, run } from './run'; @@ -75,18 +76,41 @@ run( } const config = await mergeConfigs(includeConfig); - const defaultMessages = await extractDefaultMessages({ path, config }); + const list = new Listr([ + { + title: 'Extracting Default Messages', + task: () => new Listr(extractDefaultMessages({ path, config }), { exitOnError: true }), + }, + { + title: 'Intregrating Locale File', + task: async ({ messages }) => { + await integrateLocaleFiles(messages, { + sourceFileName: source, + targetFileName: target, + dryRun, + ignoreIncompatible, + ignoreUnused, + ignoreMissing, + config, + log, + }); + }, + }, + ]); - await integrateLocaleFiles(defaultMessages, { - sourceFileName: source, - targetFileName: target, - dryRun, - ignoreIncompatible, - ignoreUnused, - ignoreMissing, - config, - log, - }); + try { + const reporter = new ErrorReporter(); + const messages: Map = new Map(); + await list.run({ messages, reporter }); + } catch (error) { + process.exitCode = 1; + if (error instanceof ErrorReporter) { + error.errors.forEach((e: string | Error) => log.error(e)); + } else { + log.error('Unhandled exception!'); + log.error(error); + } + } }, { flags: { diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index 1ab3c59e48376..b775a2e8e6765 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -26,7 +26,7 @@ import wreck from '@hapi/wreck'; import { deleteIndex } from './delete_index'; import { collectUiExports } from '../../../legacy/ui/ui_exports'; -import { KibanaMigrator } from '../../../legacy/server/saved_objects/migrations'; +import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; import { findPluginSpecs } from '../../../legacy/plugin_discovery'; /** diff --git a/src/legacy/core_plugins/console/public/src/__tests__/app.js b/src/legacy/core_plugins/console/public/src/__tests__/app.js index fec4d68964f0c..60c6f7a8ebb54 100644 --- a/src/legacy/core_plugins/console/public/src/__tests__/app.js +++ b/src/legacy/core_plugins/console/public/src/__tests__/app.js @@ -24,7 +24,7 @@ import history from '../history'; import mappings from '../mappings'; import init from '../app'; -describe('app initialization', () => { +describe('console app initialization', () => { const sandbox = sinon.createSandbox(); let inputMock; @@ -34,7 +34,7 @@ describe('app initialization', () => { ajaxDoneStub = sinon.stub(); sandbox.stub($, 'ajax').returns({ done: ajaxDoneStub }); sandbox.stub(history, 'getSavedEditorState'); - sandbox.stub(mappings, 'startRetrievingAutoCompleteInfo'); + sandbox.stub(mappings, 'retrieveAutoCompleteInfo'); inputMock = { update: sinon.stub(), diff --git a/src/legacy/core_plugins/console/public/src/app.js b/src/legacy/core_plugins/console/public/src/app.js index 26d0b18bb0901..c591de73d0056 100644 --- a/src/legacy/core_plugins/console/public/src/app.js +++ b/src/legacy/core_plugins/console/public/src/app.js @@ -129,7 +129,8 @@ export default function init(input, output, sourceLocation = 'stored') { input.moveCursorTo(pos.row + prefix.length, 0); input.focus(); }; + setupAutosave(); loadSavedState(); - mappings.startRetrievingAutoCompleteInfo(); + mappings.retrieveAutoCompleteInfo(); } diff --git a/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js b/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js index 5c4f09b0086e1..ce39a005750b2 100644 --- a/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js +++ b/src/legacy/core_plugins/console/public/src/controllers/sense_controller.js @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import { applyResizeCheckerToEditors } from '../sense_editor_resize'; import $ from 'jquery'; @@ -36,7 +36,6 @@ module.run(function ($rootScope) { }); module.controller('SenseController', function SenseController(Private, $scope, $timeout, $location, kbnUiAceKeyboardModeService) { - const docTitle = Private(DocTitleProvider); docTitle.change('Console'); $scope.topNavController = Private(SenseTopNavController); diff --git a/src/legacy/core_plugins/console/public/src/directives/sense_settings.js b/src/legacy/core_plugins/console/public/src/directives/sense_settings.js index 8405990af460d..9337d6f6e21fe 100644 --- a/src/legacy/core_plugins/console/public/src/directives/sense_settings.js +++ b/src/legacy/core_plugins/console/public/src/directives/sense_settings.js @@ -20,7 +20,8 @@ require('ui/directives/input_focus'); import template from './settings.html'; -const mappings = require('../mappings'); +import { getAutocomplete, getCurrentSettings, updateSettings, getPolling } from '../settings'; +import mappings from '../mappings'; require('ui/modules') .get('app/sense') @@ -30,22 +31,46 @@ require('ui/modules') template, controllerAs: 'settings', controller: function ($scope, $element) { - const settings = require('../settings'); + this.vals = getCurrentSettings(); - this.vals = settings.getCurrentSettings(); - this.apply = () => { - const prevSettings = settings.getAutocomplete(); - this.vals = settings.updateSettings(this.vals); - // Find which, if any, autocomplete settings have changed - const settingsDiff = Object.keys(prevSettings).filter(key => prevSettings[key] !== this.vals.autocomplete[key]); - if (settingsDiff.length > 0) { + this.isPollingVisible = () => { + const selectedAutoCompleteOptions = + Object.keys(this.vals.autocomplete).filter(key => this.vals.autocomplete[key]); + return selectedAutoCompleteOptions.length > 0; + }; + + this.refresh = () => { + mappings.retrieveAutoCompleteInfo(); + }; + + this.saveSettings = () => { + const prevSettings = getAutocomplete(); + const prevPolling = getPolling(); + + this.vals = updateSettings(this.vals); + + // We'll only retrieve settings if polling is on. + if (getPolling()) { + // Find which, if any, autocomplete settings have changed. + const settingsDiff = Object.keys(prevSettings).filter(key => prevSettings[key] !== this.vals.autocomplete[key]); const changedSettings = settingsDiff.reduce((changedSettingsAccum, setting) => { changedSettingsAccum[setting] = this.vals.autocomplete[setting]; return changedSettingsAccum; }, {}); - // Update autocomplete info based on changes so new settings takes effect immediately. - mappings.retrieveAutoCompleteInfo(changedSettings); + + const isSettingsChanged = settingsDiff.length > 0; + const isPollingChanged = prevPolling !== getPolling(); + + if (isSettingsChanged) { + // If the user has changed one of the autocomplete settings, then we'll fetch just the + // ones which have changed. + mappings.retrieveAutoCompleteInfo(changedSettings); + } else if (isPollingChanged) { + // If the user has turned polling on, then we'll fetch all selected autocomplete settings. + mappings.retrieveAutoCompleteInfo(); + } } + $scope.kbnTopNav.close(); }; @@ -53,7 +78,7 @@ require('ui/modules') function onEnter(event) { if (event.which === 13) { - self.apply(); + self.saveSettings(); } } diff --git a/src/legacy/core_plugins/console/public/src/directives/settings.html b/src/legacy/core_plugins/console/public/src/directives/settings.html index 5e82242eaa1fb..52e8ce77d3451 100644 --- a/src/legacy/core_plugins/console/public/src/directives/settings.html +++ b/src/legacy/core_plugins/console/public/src/directives/settings.html @@ -4,7 +4,7 @@ i18n-default-message="Settings" > -
+
@@ -44,6 +44,33 @@
+
+
+ +
+ + +
+
+
+
+ +
+ +

+ + + + +
+
+ class="euiButton euiButton--primary" + > + + + + + > + + + +
diff --git a/src/legacy/core_plugins/console/public/src/input.js b/src/legacy/core_plugins/console/public/src/input.js index 64cb27b51e7f7..36a6c52fd17b8 100644 --- a/src/legacy/core_plugins/console/public/src/input.js +++ b/src/legacy/core_plugins/console/public/src/input.js @@ -20,6 +20,7 @@ require('brace'); require('brace/ext/searchbox'); import Autocomplete from './autocomplete'; +import mappings from './mappings'; const SenseEditor = require('./sense_editor/editor'); const settings = require('./settings'); const utils = require('./utils'); @@ -110,13 +111,9 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu if (reqId !== CURRENT_REQ_ID) { return; } - let xhr; - if (dataOrjqXHR.promise) { - xhr = dataOrjqXHR; - } - else { - xhr = jqXhrORerrorThrown; - } + + const xhr = dataOrjqXHR.promise ? dataOrjqXHR : jqXhrORerrorThrown; + function modeForContentType(contentType) { if (contentType.indexOf('text/plain') >= 0) { return 'ace/mode/text'; @@ -127,18 +124,27 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu return null; } - if (typeof xhr.status === 'number' && - // things like DELETE index where the index is not there are OK. - ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404) - ) { + const isSuccess = typeof xhr.status === 'number' && + // Things like DELETE index where the index is not there are OK. + ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404); + + if (isSuccess) { + if (xhr.status !== 404 && settings.getPolling()) { + // If the user has submitted a request against ES, something in the fields, indices, aliases, + // or templates may have changed, so we'll need to update this data. Assume that if + // the user disables polling they're trying to optimize performance or otherwise + // preserve resources, so they won't want this request sent either. + mappings.retrieveAutoCompleteInfo(); + } + // we have someone on the other side. Add to history history.addToHistory(esPath, esMethod, esData); - let value = xhr.responseText; const mode = modeForContentType(xhr.getAllResponseHeaders('Content-Type') || ''); - if (mode === null || mode === 'application/json') { + // Apply triple quotes to output. + if (settings.getTripleQuotes() && (mode === null || mode === 'application/json')) { // assume json - auto pretty try { value = utils.expandLiteralStrings(value); @@ -166,8 +172,7 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu isFirstRequest = false; // single request terminate via sendNextRequest as well sendNextRequest(); - } - else { + } else { let value; let mode; if (xhr.responseText) { diff --git a/src/legacy/core_plugins/console/public/src/mappings.js b/src/legacy/core_plugins/console/public/src/mappings.js index d085fe4d101f7..36e8d04291f62 100644 --- a/src/legacy/core_plugins/console/public/src/mappings.js +++ b/src/legacy/core_plugins/console/public/src/mappings.js @@ -22,7 +22,10 @@ const _ = require('lodash'); const es = require('./es'); const settings = require('./settings'); - +// NOTE: If this value ever changes to be a few seconds or less, it might introduce flakiness +// due to timing issues in our app.js tests. +const POLL_INTERVAL = 60000; +let pollTimeoutId; let perIndexTypes = {}; let perAliasIndexes = []; @@ -61,12 +64,13 @@ function expandAliases(indicesOrAliases) { function getTemplates() { return [ ...templates ]; } + function getFields(indices, types) { // get fields for indices and types. Both can be a list, a string or null (meaning all). let ret = []; indices = expandAliases(indices); - if (typeof indices === 'string') { + if (typeof indices === 'string') { const typeDict = perIndexTypes[indices]; if (!typeDict) { return []; @@ -75,8 +79,7 @@ function getFields(indices, types) { if (typeof types === 'string') { const f = typeDict[types]; ret = f ? f : []; - } - else { + } else { // filter what we need $.each(typeDict, function (type, fields) { if (!types || types.length === 0 || $.inArray(type, types) !== -1) { @@ -86,8 +89,7 @@ function getFields(indices, types) { ret = [].concat.apply([], ret); } - } - else { + } else { // multi index mode. $.each(perIndexTypes, function (index) { if (!indices || indices.length === 0 || $.inArray(index, indices) !== -1) { @@ -128,10 +130,8 @@ function getTypes(indices) { } return _.uniq(ret); - } - function getIndices(includeAliases) { const ret = []; $.each(perIndexTypes, function (index) { @@ -164,7 +164,7 @@ function getFieldNamesFromFieldMapping(fieldName, fieldMapping) { if (fieldMapping.properties) { // derived object type - nestedFields = getFieldNamesFromTypeMapping(fieldMapping); + nestedFields = getFieldNamesFromProperties(fieldMapping.properties); return applyPathSettings(nestedFields); } @@ -196,9 +196,9 @@ function getFieldNamesFromFieldMapping(fieldName, fieldMapping) { return [ret]; } -function getFieldNamesFromTypeMapping(typeMapping) { +function getFieldNamesFromProperties(properties = {}) { const fieldList = - $.map(typeMapping.properties || {}, function (fieldMapping, fieldName) { + $.map(properties, function (fieldMapping, fieldName) { return getFieldNamesFromFieldMapping(fieldName, fieldMapping); }); @@ -214,16 +214,24 @@ function loadTemplates(templatesObject = {}) { function loadMappings(mappings) { perIndexTypes = {}; + $.each(mappings, function (index, indexMapping) { const normalizedIndexMappings = {}; - // 1.0.0 mapping format has changed, extract underlying mapping + + // Migrate 1.0.0 mappings. This format has changed, so we need to extract the underlying mapping. if (indexMapping.mappings && _.keys(indexMapping).length === 1) { indexMapping = indexMapping.mappings; } + $.each(indexMapping, function (typeName, typeMapping) { - const fieldList = getFieldNamesFromTypeMapping(typeMapping); - normalizedIndexMappings[typeName] = fieldList; + if (typeName === 'properties') { + const fieldList = getFieldNamesFromProperties(typeMapping); + normalizedIndexMappings[typeName] = fieldList; + } else { + normalizedIndexMappings[typeName] = []; + } }); + perIndexTypes[index] = normalizedIndexMappings; }); } @@ -256,20 +264,21 @@ function clear() { templates = []; } -function retrieveSettings(settingsKey, changedFields) { - const autocompleteSettings = settings.getAutocomplete(); +function retrieveSettings(settingsKey, settingsToRetrieve) { + const currentSettings = settings.getAutocomplete(); const settingKeyToPathMap = { fields: '_mapping', indices: '_aliases', templates: '_template', }; - // Fetch autocomplete info if setting is set to true, and if user has made changes - if (autocompleteSettings[settingsKey] && changedFields[settingsKey]) { + + // Fetch autocomplete info if setting is set to true, and if user has made changes. + if (currentSettings[settingsKey] && settingsToRetrieve[settingsKey]) { return es.send('GET', settingKeyToPathMap[settingsKey], null, null, true); } else { const settingsPromise = new $.Deferred(); // If a user has saved settings, but a field remains checked and unchanged, no need to make changes - if (autocompleteSettings[settingsKey]) { + if (currentSettings[settingsKey]) { return settingsPromise.resolve(); } // If the user doesn't want autocomplete suggestions, then clear any that exist @@ -277,10 +286,15 @@ function retrieveSettings(settingsKey, changedFields) { } } -function retrieveAutocompleteInfoFromServer(changedFields) { - const mappingPromise = retrieveSettings('fields', changedFields); - const aliasesPromise = retrieveSettings('indices', changedFields); - const templatesPromise = retrieveSettings('templates', changedFields); +// Retrieve all selected settings by default. +function retrieveAutoCompleteInfo(settingsToRetrieve = settings.getAutocomplete()) { + if (pollTimeoutId) { + clearTimeout(pollTimeoutId); + } + + const mappingPromise = retrieveSettings('fields', settingsToRetrieve); + const aliasesPromise = retrieveSettings('indices', settingsToRetrieve); + const templatesPromise = retrieveSettings('templates', settingsToRetrieve); $.when(mappingPromise, aliasesPromise, templatesPromise) .done((mappings, aliases, templates) => { @@ -308,26 +322,26 @@ function retrieveAutocompleteInfoFromServer(changedFields) { // Trigger an update event with the mappings, aliases $(mappingObj).trigger('update', [mappingsResponse, aliases[0]]); } - }); -} -function autocompleteRetriever() { - const changedFields = settings.getAutocomplete(); - retrieveAutocompleteInfoFromServer(changedFields); - setTimeout(function () { - autocompleteRetriever(); - }, 60000); + // Schedule next request. + pollTimeoutId = setTimeout(() => { + // This looks strange/inefficient, but it ensures correct behavior because we don't want to send + // a scheduled request if the user turns off polling. + if (settings.getPolling()) { + retrieveAutoCompleteInfo(); + } + }, POLL_INTERVAL); + }); } -export default _.assign(mappingObj, { - getFields: getFields, - getTemplates: getTemplates, - getIndices: getIndices, - getTypes: getTypes, - loadMappings: loadMappings, - loadAliases: loadAliases, - expandAliases: expandAliases, - clear: clear, - startRetrievingAutoCompleteInfo: autocompleteRetriever, - retrieveAutoCompleteInfo: retrieveAutocompleteInfoFromServer -}); +export default { + getFields, + getTemplates, + getIndices, + getTypes, + loadMappings, + loadAliases, + expandAliases, + clear, + retrieveAutoCompleteInfo, +}; diff --git a/src/legacy/core_plugins/console/public/src/settings.js b/src/legacy/core_plugins/console/public/src/settings.js index e8920e8d0ed1b..23e89d00563a3 100644 --- a/src/legacy/core_plugins/console/public/src/settings.js +++ b/src/legacy/core_plugins/console/public/src/settings.js @@ -42,6 +42,15 @@ function setWrapMode(mode) { return true; } +function setTripleQuotes(tripleQuotes) { + storage.set('triple_quotes', tripleQuotes); + return true; +} + +export function getTripleQuotes() { + return storage.get('triple_quotes', true); +} + export function getAutocomplete() { return storage.get('autocomplete_settings', { fields: true, indices: true, templates: true }); } @@ -51,6 +60,16 @@ function setAutocomplete(settings) { return true; } +export function getPolling() { + return storage.get('console_polling', true); +} + +function setPolling(polling) { + storage.set('console_polling', polling); + applyCurrentSettings(); + return true; +} + export function applyCurrentSettings(editor) { if (typeof editor === 'undefined') { applyCurrentSettings(getInput()); @@ -66,14 +85,18 @@ export function getCurrentSettings() { return { autocomplete: getAutocomplete(), wrapMode: getWrapMode(), + tripleQuotes: getTripleQuotes(), fontSize: parseFloat(getFontSize()), + polling: Boolean(getPolling()), }; } -export function updateSettings({ fontSize, wrapMode, autocomplete }) { +export function updateSettings({ fontSize, wrapMode, tripleQuotes, autocomplete, polling }) { setFontSize(fontSize); setWrapMode(wrapMode); + setTripleQuotes(tripleQuotes); setAutocomplete(autocomplete); + setPolling(polling); getInput().focus(); return getCurrentSettings(); } diff --git a/src/legacy/core_plugins/console/public/tests/src/integration.test.js b/src/legacy/core_plugins/console/public/tests/src/integration.test.js index cfb6e56bf5efa..0164f291594f3 100644 --- a/src/legacy/core_plugins/console/public/tests/src/integration.test.js +++ b/src/legacy/core_plugins/console/public/tests/src/integration.test.js @@ -201,7 +201,7 @@ describe('Integration', () => { endpoints: { _search: { methods: ['GET', 'POST'], - patterns: ['{indices}/{types}/_search', '{indices}/_search', '_search'], + patterns: ['{indices}/_search', '_search'], data_autocomplete_rules: { query: { match_all: {}, @@ -221,19 +221,15 @@ describe('Integration', () => { const MAPPING = { index1: { - 'type1.1': { - properties: { - 'field1.1.1': { type: 'string' }, - 'field1.1.2': { type: 'string' }, - }, + properties: { + 'field1.1.1': { type: 'string' }, + 'field1.1.2': { type: 'string' }, }, }, index2: { - 'type2.1': { - properties: { - 'field2.1.1': { type: 'string' }, - 'field2.1.2': { type: 'string' }, - }, + properties: { + 'field2.1.1': { type: 'string' }, + 'field2.1.2': { type: 'string' }, }, }, }; @@ -710,6 +706,9 @@ describe('Integration', () => { ] ); + // NOTE: This test emits "error while getting completion terms Error: failed to resolve link + // [GLOBAL.broken]: Error: failed to resolve global components for ['broken']". but that's + // expected. contextTests( { a: { @@ -980,6 +979,7 @@ describe('Integration', () => { ] ); + // NOTE: This test emits "Can't extract a valid url token path", but that's expected. contextTests('POST _search\n', MAPPING, SEARCH_KB, null, [ { name: 'initial doc start', @@ -1014,7 +1014,7 @@ describe('Integration', () => { const CLUSTER_KB = { endpoints: { _search: { - patterns: ['_search', '{indices}/{types}/_search', '{indices}/_search'], + patterns: ['_search', '{indices}/_search'], url_params: { search_type: ['count', 'query_then_fetch'], scroll: '10m', diff --git a/src/legacy/core_plugins/console/public/tests/src/mapping.test.js b/src/legacy/core_plugins/console/public/tests/src/mapping.test.js index 4c16c74d3f30a..c5ef5ed43bb3d 100644 --- a/src/legacy/core_plugins/console/public/tests/src/mapping.test.js +++ b/src/legacy/core_plugins/console/public/tests/src/mapping.test.js @@ -47,23 +47,21 @@ describe('Mappings', () => { test('Multi fields', function () { mappings.loadMappings({ index: { - tweet: { - properties: { - first_name: { - type: 'multi_field', - path: 'just_name', - fields: { - first_name: { type: 'string', index: 'analyzed' }, - any_name: { type: 'string', index: 'analyzed' }, - }, + properties: { + first_name: { + type: 'multi_field', + path: 'just_name', + fields: { + first_name: { type: 'string', index: 'analyzed' }, + any_name: { type: 'string', index: 'analyzed' }, }, - last_name: { - type: 'multi_field', - path: 'just_name', - fields: { - last_name: { type: 'string', index: 'analyzed' }, - any_name: { type: 'string', index: 'analyzed' }, - }, + }, + last_name: { + type: 'multi_field', + path: 'just_name', + fields: { + last_name: { type: 'string', index: 'analyzed' }, + any_name: { type: 'string', index: 'analyzed' }, }, }, }, @@ -80,22 +78,20 @@ describe('Mappings', () => { test('Multi fields 1.0 style', function () { mappings.loadMappings({ index: { - tweet: { - properties: { - first_name: { - type: 'string', - index: 'analyzed', - path: 'just_name', - fields: { - any_name: { type: 'string', index: 'analyzed' }, - }, + properties: { + first_name: { + type: 'string', + index: 'analyzed', + path: 'just_name', + fields: { + any_name: { type: 'string', index: 'analyzed' }, }, - last_name: { - type: 'string', - index: 'no', - fields: { - raw: { type: 'string', index: 'analyzed' }, - }, + }, + last_name: { + type: 'string', + index: 'no', + fields: { + raw: { type: 'string', index: 'analyzed' }, }, }, }, @@ -113,14 +109,12 @@ describe('Mappings', () => { test('Simple fields', function () { mappings.loadMappings({ index: { - tweet: { - properties: { - str: { - type: 'string', - }, - number: { - type: 'int', - }, + properties: { + str: { + type: 'string', + }, + number: { + type: 'int', }, }, }, @@ -136,14 +130,12 @@ describe('Mappings', () => { mappings.loadMappings({ index: { mappings: { - tweet: { - properties: { - str: { - type: 'string', - }, - number: { - type: 'int', - }, + properties: { + str: { + type: 'string', + }, + number: { + type: 'int', }, }, }, @@ -159,27 +151,25 @@ describe('Mappings', () => { test('Nested fields', function () { mappings.loadMappings({ index: { - tweet: { - properties: { - person: { - type: 'object', - properties: { - name: { - properties: { - first_name: { type: 'string' }, - last_name: { type: 'string' }, - }, + properties: { + person: { + type: 'object', + properties: { + name: { + properties: { + first_name: { type: 'string' }, + last_name: { type: 'string' }, }, - sid: { type: 'string', index: 'not_analyzed' }, }, + sid: { type: 'string', index: 'not_analyzed' }, }, - message: { type: 'string' }, }, + message: { type: 'string' }, }, }, }); - expect(mappings.getFields('index', ['tweet']).sort(fc)).toEqual([ + expect(mappings.getFields('index', []).sort(fc)).toEqual([ f('message'), f('person.name.first_name'), f('person.name.last_name'), @@ -190,25 +180,23 @@ describe('Mappings', () => { test('Enabled fields', function () { mappings.loadMappings({ index: { - tweet: { - properties: { - person: { - type: 'object', - properties: { - name: { - type: 'object', - enabled: false, - }, - sid: { type: 'string', index: 'not_analyzed' }, + properties: { + person: { + type: 'object', + properties: { + name: { + type: 'object', + enabled: false, }, + sid: { type: 'string', index: 'not_analyzed' }, }, - message: { type: 'string' }, }, + message: { type: 'string' }, }, }, }); - expect(mappings.getFields('index', ['tweet']).sort(fc)).toEqual([ + expect(mappings.getFields('index', []).sort(fc)).toEqual([ f('message'), f('person.sid'), ]); @@ -217,23 +205,21 @@ describe('Mappings', () => { test('Path tests', function () { mappings.loadMappings({ index: { - person: { - properties: { - name1: { - type: 'object', - path: 'just_name', - properties: { - first1: { type: 'string' }, - last1: { type: 'string', index_name: 'i_last_1' }, - }, + properties: { + name1: { + type: 'object', + path: 'just_name', + properties: { + first1: { type: 'string' }, + last1: { type: 'string', index_name: 'i_last_1' }, }, - name2: { - type: 'object', - path: 'full', - properties: { - first2: { type: 'string' }, - last2: { type: 'string', index_name: 'i_last_2' }, - }, + }, + name2: { + type: 'object', + path: 'full', + properties: { + first2: { type: 'string' }, + last2: { type: 'string', index_name: 'i_last_2' }, }, }, }, @@ -251,10 +237,8 @@ describe('Mappings', () => { test('Use index_name tests', function () { mappings.loadMappings({ index: { - person: { - properties: { - last1: { type: 'string', index_name: 'i_last_1' }, - }, + properties: { + last1: { type: 'string', index_name: 'i_last_1' }, }, }, }); @@ -284,17 +268,13 @@ describe('Mappings', () => { }); mappings.loadMappings({ test_index1: { - type1: { - properties: { - last1: { type: 'string', index_name: 'i_last_1' }, - }, + properties: { + last1: { type: 'string', index_name: 'i_last_1' }, }, }, test_index2: { - type2: { - properties: { - last1: { type: 'string', index_name: 'i_last_1' }, - }, + properties: { + last1: { type: 'string', index_name: 'i_last_1' }, }, }, }); diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts index f22caf3d43ece..bb2ca806bbac3 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -24,13 +24,15 @@ import { Ast } from '@kbn/interpreter/common'; // the interpreter plugin itself once they are ready import { Registry } from '@kbn/interpreter/common'; import { Adapters } from 'ui/inspector'; -import { Query, Filters, TimeRange } from 'ui/embeddable'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Filter } from '@kbn/es-query'; import { createRenderer } from './expression_renderer'; import { createRunFn } from './expression_runner'; +import { Query } from '../query'; export interface InitialContextObject { timeRange?: TimeRange; - filters?: Filters; + filters?: Filter[]; query?: Query; } diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index b115bbdb1651b..539a21a22c841 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -36,7 +36,6 @@ import * as types from 'ui/index_patterns'; const config = chrome.getUiSettingsClient(); const savedObjectsClient = chrome.getSavedObjectsClient(); -const basePath = chrome.getBasePath(); /** * Index Patterns Service * @@ -51,7 +50,7 @@ const basePath = chrome.getBasePath(); export class IndexPatternsService { public setup() { return { - indexPatterns: new IndexPatterns(basePath, config, savedObjectsClient), + indexPatterns: new IndexPatterns(config, savedObjectsClient), }; } diff --git a/src/legacy/core_plugins/elasticsearch/lib/cluster.ts b/src/legacy/core_plugins/elasticsearch/lib/cluster.ts index 873b9c8f8b59c..a595fffb3c235 100644 --- a/src/legacy/core_plugins/elasticsearch/lib/cluster.ts +++ b/src/legacy/core_plugins/elasticsearch/lib/cluster.ts @@ -17,8 +17,9 @@ * under the License. */ +import { Request } from 'hapi'; import { errors } from 'elasticsearch'; -import { CallAPIOptions, ClusterClient } from 'kibana/server'; +import { CallAPIOptions, ClusterClient, FakeRequest } from 'kibana/server'; export class Cluster { public readonly errors = errors; @@ -26,7 +27,7 @@ export class Cluster { constructor(private readonly clusterClient: ClusterClient) {} public callWithRequest = async ( - req: { headers?: Record } = {}, + req: Request | FakeRequest, endpoint: string, clientParams?: Record, options?: CallAPIOptions diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts index 67071cc171b5d..88f24be36b117 100644 --- a/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/actions/apply_filter_action.test.ts @@ -39,7 +39,7 @@ beforeAll(() => { }); afterAll(() => { - embeddableFactories.reset(); + embeddableFactories.clear(); }); test('ApplyFilterAction applies the filter to the root of the container tree', async () => { diff --git a/src/legacy/core_plugins/embeddable_api/public/actions/index.ts b/src/legacy/core_plugins/embeddable_api/public/actions/index.ts index 81964b62520bc..a0e71340eee23 100644 --- a/src/legacy/core_plugins/embeddable_api/public/actions/index.ts +++ b/src/legacy/core_plugins/embeddable_api/public/actions/index.ts @@ -19,7 +19,5 @@ export { Action, ActionContext } from './action'; export { IncompatibleActionError } from './incompatible_action_error'; -import { createRegistry } from '../create_registry'; import { Action } from './action'; - -export const actionRegistry = createRegistry(); +export const actionRegistry = new Map(); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts index 58e8e74b4c178..3ac23902f11e0 100644 --- a/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/containers/container.test.ts @@ -38,7 +38,6 @@ import { import { isErrorEmbeddable, EmbeddableOutput, EmbeddableFactory } from '../embeddables'; import { ContainerInput } from './i_container'; import { ViewMode } from '../types'; -import { createRegistry } from '../create_registry'; import { FilterableEmbeddableInput, FilterableEmbeddable, @@ -47,7 +46,7 @@ import { ERROR_EMBEDDABLE_TYPE } from '../embeddables/error_embeddable'; import { Filter, FilterStateStore } from '@kbn/es-query'; import { PanelNotFoundError } from './panel_not_found_error'; -const embeddableFactories = createRegistry(); +const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory()); embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory()); @@ -555,7 +554,7 @@ test('Container changes made directly after adding a new embeddable are propagat embeddableFactories ); - embeddableFactories.reset(); + embeddableFactories.clear(); embeddableFactories.set( CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory({ loadTickCount: 3 }) @@ -678,7 +677,7 @@ test('untilEmbeddableLoaded throws an error if there is no such child panel in t }); test('untilEmbeddableLoaded resolves if child is has an type that does not exist', async done => { - embeddableFactories.reset(); + embeddableFactories.clear(); const container = new HelloWorldContainer( { id: 'hello', @@ -699,7 +698,7 @@ test('untilEmbeddableLoaded resolves if child is has an type that does not exist }); test('untilEmbeddableLoaded resolves if child is loaded in the container', async done => { - embeddableFactories.reset(); + embeddableFactories.clear(); embeddableFactories.set(HELLO_WORLD_EMBEDDABLE_TYPE, new HelloWorldEmbeddableFactory()); const container = new HelloWorldContainer( @@ -722,7 +721,7 @@ test('untilEmbeddableLoaded resolves if child is loaded in the container', async }); test('untilEmbeddableLoaded rejects with an error if child is subsequently removed', async done => { - embeddableFactories.reset(); + embeddableFactories.clear(); embeddableFactories.set( CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory({ loadTickCount: 3 }) diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/container.ts b/src/legacy/core_plugins/embeddable_api/public/containers/container.ts index e848c0a23d828..0fd53e4f335af 100644 --- a/src/legacy/core_plugins/embeddable_api/public/containers/container.ts +++ b/src/legacy/core_plugins/embeddable_api/public/containers/container.ts @@ -29,7 +29,6 @@ import { } from '../embeddables'; import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { IEmbeddable } from '../embeddables/i_embeddable'; -import { IRegistry } from '../types'; import { PanelNotFoundError } from './panel_not_found_error'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -44,14 +43,14 @@ export abstract class Container< protected readonly children: { [key: string]: IEmbeddable | ErrorEmbeddable; } = {}; - public readonly embeddableFactories: IRegistry; + public readonly embeddableFactories: Map; private subscription: Subscription; constructor( input: TContainerInput, output: TContainerOutput, - embeddableFactories: IRegistry, + embeddableFactories: Map, parent?: Container ) { super(input, output, parent); diff --git a/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts b/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts index 1c8374d193e98..fb87062af1fd8 100644 --- a/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts +++ b/src/legacy/core_plugins/embeddable_api/public/containers/i_container.ts @@ -25,7 +25,6 @@ import { EmbeddableFactory, } from '../embeddables'; import { IEmbeddable } from '../embeddables/i_embeddable'; -import { IRegistry } from '../types'; export interface PanelState< E extends { [key: string]: unknown } & { id: string } = { [key: string]: unknown } & { @@ -59,7 +58,7 @@ export interface IContainer< I extends ContainerInput = ContainerInput, O extends ContainerOutput = ContainerOutput > extends IEmbeddable { - readonly embeddableFactories: IRegistry; + readonly embeddableFactories: Map; /** * Call if you want to wait until an embeddable with that id has finished loading. diff --git a/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts index 5187b929373de..a22d22615cf95 100644 --- a/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts +++ b/src/legacy/core_plugins/embeddable_api/public/context_menu_actions/build_eui_context_menu_panels.ts @@ -42,7 +42,7 @@ export async function buildContextMenuForActions({ return { id: 'mainMenu', - title: i18n.translate('embeddableAPI.actionPanel.title', { + title: i18n.translate('embeddableApi.actionPanel.title', { defaultMessage: 'Options', }), items: menuItems, diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factories_registry.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factories_registry.ts index ca648a62b412b..c8ebd942d49eb 100644 --- a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factories_registry.ts +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factories_registry.ts @@ -18,6 +18,4 @@ */ import { EmbeddableFactory } from './embeddable_factory'; -import { createRegistry } from '../create_registry'; - -export const embeddableFactories = createRegistry(); +export const embeddableFactories = new Map(); diff --git a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts index 545514a452f95..0fa7724e7eccd 100644 --- a/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts +++ b/src/legacy/core_plugins/embeddable_api/public/embeddables/embeddable_factory.ts @@ -18,7 +18,7 @@ */ import { SavedObjectMetaData } from 'ui/saved_objects/components/saved_object_finder'; -import { SavedObjectAttributes } from '../../../../server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server'; import { EmbeddableInput, EmbeddableOutput } from './i_embeddable'; import { ErrorEmbeddable } from './error_embeddable'; import { IContainer } from '../containers/i_container'; diff --git a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.test.ts b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.test.ts index 14fafacd5de69..eccde1c3d59a7 100644 --- a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.test.ts @@ -33,13 +33,13 @@ import { getActionsForTrigger } from './get_actions_for_trigger'; import { attachAction } from './triggers/attach_action'; beforeEach(() => { - actionRegistry.reset(); - triggerRegistry.reset(); + actionRegistry.clear(); + triggerRegistry.clear(); }); afterAll(() => { - actionRegistry.reset(); - triggerRegistry.reset(); + actionRegistry.clear(); + triggerRegistry.clear(); }); test('ActionRegistry adding and getting an action', async () => { @@ -49,8 +49,7 @@ test('ActionRegistry adding and getting an action', async () => { actionRegistry.set(sayHelloAction.id, sayHelloAction); actionRegistry.set(helloWorldAction.id, helloWorldAction); - expect(actionRegistry.length()).toBe(2); - + expect(actionRegistry.size).toBe(2); expect(actionRegistry.get(sayHelloAction.id)).toBe(sayHelloAction); expect(actionRegistry.get(helloWorldAction.id)).toBe(helloWorldAction); }); diff --git a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts index 6574e10d6002c..eea740aca2767 100644 --- a/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts +++ b/src/legacy/core_plugins/embeddable_api/public/get_actions_for_trigger.ts @@ -20,11 +20,11 @@ import { Action } from './actions'; import { IEmbeddable } from './embeddables'; import { IContainer } from './containers'; -import { IRegistry, Trigger } from './types'; +import { Trigger } from './types'; export async function getActionsForTrigger( - actionRegistry: IRegistry, - triggerRegistry: IRegistry, + actionRegistry: Map, + triggerRegistry: Map, triggerId: string, context: { embeddable: IEmbeddable; container?: IContainer } ) { diff --git a/src/legacy/core_plugins/embeddable_api/public/index.ts b/src/legacy/core_plugins/embeddable_api/public/index.ts index cdf0ce7e92ef3..92cfe0a369c21 100644 --- a/src/legacy/core_plugins/embeddable_api/public/index.ts +++ b/src/legacy/core_plugins/embeddable_api/public/index.ts @@ -29,7 +29,7 @@ export { isErrorEmbeddable, } from './embeddables'; -export { Query, TimeRange, ViewMode, QueryLanguageType, Trigger, IRegistry } from './types'; +export { ViewMode, Trigger } from './types'; export { actionRegistry, Action, ActionContext, IncompatibleActionError } from './actions'; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx index a0207a1de0c2b..489ee970f9deb 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/embeddable_panel.test.tsx @@ -45,7 +45,6 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; import { attachAction } from '../triggers/attach_action'; -import { createRegistry } from '../create_registry'; import { EmbeddableFactory } from '../embeddables'; const editModeAction = new EditModeAction(); @@ -55,7 +54,7 @@ attachAction(triggerRegistry, { actionId: editModeAction.id, }); -const embeddableFactories = createRegistry(); +const embeddableFactories = new Map(); embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); test('HelloWorldContainer initializes embeddables', async done => { diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx index c375494d497c8..e08f806f8477f 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -30,11 +30,10 @@ import { import { ViewMode, EmbeddableOutput, isErrorEmbeddable } from '../../../../'; import { AddPanelAction } from './add_panel_action'; -import { createRegistry } from '../../../../create_registry'; import { EmbeddableFactory } from '../../../../embeddables'; import { Filter, FilterStateStore } from '@kbn/es-query'; -const embeddableFactories = createRegistry(); +const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); let container: FilterableContainer; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.tsx index 859194cc6f673..5f0571f3d140c 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_action.tsx @@ -35,7 +35,7 @@ export class AddPanelAction extends Action { } public getDisplayName() { - return i18n.translate('kbn.embeddable.panel.addPanel.displayName', { + return i18n.translate('embeddableApi.addPanel.displayName', { defaultMessage: 'Add panel', }); } diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 42289ab65df27..137bf36d0230c 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -35,14 +35,13 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { skip } from 'rxjs/operators'; import * as Rx from 'rxjs'; -import { createRegistry } from '../../../../create_registry'; import { EmbeddableFactory } from '../../../../embeddables'; const onClose = jest.fn(); let container: Container; function createHelloWorldContainer(input = { id: '123', panels: {} }) { - const embeddableFactories = createRegistry(); + const embeddableFactories = new Map(); embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); return new HelloWorldContainer(input, embeddableFactories); } diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 6477378aa2bc5..1683f05c1d6b7 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -38,7 +38,7 @@ import { EuiText, } from '@elastic/eui'; -import { SavedObjectAttributes } from '../../../../../../../server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server/saved_objects'; import { EmbeddableFactoryNotFoundError } from '../../../../embeddables/embeddable_factory_not_found_error'; import { IContainer } from '../../../../containers'; @@ -63,7 +63,7 @@ export class AddPanelFlyout extends React.Component { this.lastToast = toastNotifications.addSuccess({ title: i18n.translate( - 'kbn.embeddables.addPanel.savedObjectAddedToContainerSuccessMessageTitle', + 'embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle', { defaultMessage: '{savedObjectName} was added', values: { @@ -103,15 +103,13 @@ export class AddPanelFlyout extends React.Component { inputDisplay: ( ), }, - - ...this.props.container.embeddableFactories - .getAll() + ...[...this.props.container.embeddableFactories.values()] .filter( factory => factory.isEditable() && !factory.isContainerType && factory.canCreateNew() ) @@ -119,7 +117,7 @@ export class AddPanelFlyout extends React.Component { inputDisplay: ( {

- +

@@ -147,8 +145,7 @@ export class AddPanelFlyout extends React.Component { Boolean(embeddableFactory.savedObjectMetaData) && @@ -159,7 +156,7 @@ export class AddPanelFlyout extends React.Component { > } showFilter={true} - noItemsMessage={i18n.translate('kbn.embeddables.addPanel.noMatchingObjectsMessage', { + noItemsMessage={i18n.translate('embeddableApi.addPanel.noMatchingObjectsMessage', { defaultMessage: 'No matching objects found.', })} /> diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts index eee46df447194..5215f662c6a6a 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -33,14 +33,13 @@ import { Container, isErrorEmbeddable } from '../../../..'; import { findTestSubject } from '@elastic/eui/lib/test'; import { nextTick } from 'test_utils/enzyme_helpers'; import { CustomizePanelTitleAction } from './customize_panel_action'; -import { createRegistry } from '../../../../create_registry'; import { EmbeddableFactory } from '../../../../embeddables'; let container: Container; let embeddable: ContactCardEmbeddable; function createHelloWorldContainer(input = { id: '123', panels: {} }) { - const embeddableFactories = createRegistry(); + const embeddableFactories = new Map(); embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); return new HelloWorldContainer(input, embeddableFactories); } diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.tsx index 8673ab8b6f14b..d208b941ee57c 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_action.tsx @@ -40,7 +40,7 @@ export class CustomizePanelTitleAction extends Action { } public getDisplayName() { - return i18n.translate('kbn.embeddables.panel.customizePanel.displayName', { + return i18n.translate('embeddableApi.customizePanel.action.displayName', { defaultMessage: 'Customize panel', }); } diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.test.tsx index 9dbb481c429bd..3192c4838a855 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.test.tsx @@ -34,14 +34,13 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { CustomizePanelModal } from './customize_panel_modal'; import { Container, isErrorEmbeddable } from '../../../..'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { createRegistry } from '../../../../create_registry'; import { EmbeddableFactory } from '../../../../embeddables'; let container: Container; let embeddable: ContactCardEmbeddable; beforeEach(async () => { - const embeddableFactories = createRegistry(); + const embeddableFactories = new Map(); embeddableFactories.set(CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory()); container = new HelloWorldContainer({ id: '123', panels: {} }, embeddableFactories); const contactCardEmbeddable = await container.addNewEmbeddable< diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx index 91de837fabc18..8bbc7b2f2c6e5 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_panel_modal.tsx @@ -97,7 +97,7 @@ export class CustomizePanelModalUi extends Component label={ } onChange={this.onHideTitleToggle} @@ -105,7 +105,7 @@ export class CustomizePanelModalUi extends Component @@ -119,7 +119,7 @@ export class CustomizePanelModalUi extends Component value={this.state.title || ''} onChange={e => this.updateTitle(e.target.value)} aria-label={this.props.intl.formatMessage({ - id: 'kbn.embeddable.panel.optionsMenuForm.panelTitleInputAriaLabel', + id: 'embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel', defaultMessage: 'Enter a custom title for your panel', })} append={ @@ -129,7 +129,7 @@ export class CustomizePanelModalUi extends Component disabled={this.state.hideTitle} > @@ -142,11 +142,17 @@ export class CustomizePanelModalUi extends Component onClick={() => this.props.updateTitle(this.props.embeddable.getOutput().title)} > {' '} - + - + diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx index c4b9f9ff9a40a..aa7a0b9dd781f 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/customize_title/customize_title_form.tsx @@ -46,7 +46,7 @@ function CustomizeTitleFormUi({
@@ -58,7 +58,7 @@ function CustomizeTitleFormUi({ value={title} onChange={onInputChange} aria-label={intl.formatMessage({ - id: 'kbn.dashboard.panel.optionsMenuForm.panelTitleInputAriaLabel', + id: 'embeddableApi.customizeTitle.optionsMenuForm.panelTitleInputAriaLabel', defaultMessage: 'Changes to this input are applied immediately. Press enter to exit.', })} /> @@ -66,7 +66,7 @@ function CustomizeTitleFormUi({ diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.tsx index 4e7a0a63edf33..e64a793295b35 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/edit_panel_action.tsx @@ -40,7 +40,7 @@ export class EditPanelAction extends Action { if (!factory) { throw new EmbeddableFactoryNotFoundError(embeddable.type); } - return i18n.translate('kbn.dashboard.panel.editPanel.displayName', { + return i18n.translate('embeddableApi.panel.editPanel.displayName', { defaultMessage: 'Edit {value}', values: { value: factory.getDisplayName(), diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx index d5e9a2e222424..0a0ba4f08f646 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -42,11 +42,10 @@ import { import { EmbeddableOutput, isErrorEmbeddable } from '../../..'; import { InspectPanelAction } from './inspect_panel_action'; import { Inspector, Adapters } from 'ui/inspector'; -import { createRegistry } from '../../../create_registry'; import { EmbeddableFactory } from '../../../embeddables'; import { Filter, FilterStateStore } from '@kbn/es-query'; -const embeddableFactories = createRegistry(); +const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); let container: FilterableContainer; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.tsx index 3656bdc3f83a6..2346dd76a69ce 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/inspect_panel_action.tsx @@ -34,7 +34,7 @@ export class InspectPanelAction extends Action { } public getDisplayName() { - return i18n.translate('kbn.embeddable.panel.inspectPanel.displayName', { + return i18n.translate('embeddableApi.panel.inspectPanel.displayName', { defaultMessage: 'Inspect', }); } diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx index cadcb315439b9..f8b7c9ef6fbd3 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -30,11 +30,10 @@ import { import { EmbeddableOutput, isErrorEmbeddable } from '../../../'; import { RemovePanelAction } from './remove_panel_action'; -import { createRegistry } from '../../../create_registry'; import { EmbeddableFactory } from '../../../embeddables'; import { Filter, FilterStateStore } from '@kbn/es-query'; -const embeddableFactories = createRegistry(); +const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); let container: FilterableContainer; diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.tsx index 62fc04436abda..04dcb3d132ec0 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_actions/remove_panel_action.tsx @@ -44,7 +44,7 @@ export class RemovePanelAction extends Action { } public getDisplayName() { - return i18n.translate('kbn.embeddable.panel.removePanel.displayName', { + return i18n.translate('embeddableApi.panel.removePanel.displayName', { defaultMessage: 'Delete from dashboard', }); } diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_header.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_header.tsx index bea899fd63335..3d6f82b94693d 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_header.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_header.tsx @@ -70,7 +70,7 @@ function PanelHeaderUi({ title={title} aria-label={intl.formatMessage( { - id: 'kbn.dashboard.panel.dashboardPanelAriaLabel', + id: 'embeddableApi.panel.dashboardPanelAriaLabel', defaultMessage: 'Dashboard panel: {title}', }, { diff --git a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_options_menu.tsx b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_options_menu.tsx index 351e007357e5a..dfffd0b504328 100644 --- a/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_options_menu.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/panel/panel_header/panel_options_menu.tsx @@ -84,7 +84,7 @@ class PanelOptionsMenuUi extends React.Component color="text" className="embPanel__optionsMenuButton" aria-label={intl.formatMessage({ - id: 'kbn.dashboard.panel.optionsMenu.panelOptionsButtonAriaLabel', + id: 'embeddableApi.panel.optionsMenu.panelOptionsButtonAriaLabel', defaultMessage: 'Panel options', })} data-test-subj="embeddablePanelToggleMenuIcon" diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx index 22c0364a1eb20..87fb7b1169f24 100644 --- a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -35,7 +35,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory, + embeddableFactories: Map, parent?: Container ) { super(initialInput, { embeddableLoaded: {} }, embeddableFactories, parent); diff --git a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container_factory.ts b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container_factory.ts index a413e9be43cb6..f41fad06176a2 100644 --- a/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container_factory.ts +++ b/src/legacy/core_plugins/embeddable_api/public/test_samples/embeddables/filterable_container_factory.ts @@ -29,7 +29,7 @@ export class FilterableContainerFactory extends EmbeddableFactory { public readonly type = HELLO_WORLD_CONTAINER; - constructor(input: ContainerInput, embeddableFactories: IRegistry) { + constructor(input: ContainerInput, embeddableFactories: Map) { super(input, { embeddableLoaded: {} }, embeddableFactories); } diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/attach_action.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/attach_action.ts index 711f641b0cbeb..b6b6abf2abbb2 100644 --- a/src/legacy/core_plugins/embeddable_api/public/triggers/attach_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/attach_action.ts @@ -17,10 +17,10 @@ * under the License. */ -import { IRegistry, Trigger } from '../types'; +import { Trigger } from '../types'; export function attachAction( - triggerRegistry: IRegistry, + triggerRegistry: Map, { triggerId, actionId }: { triggerId: string; actionId: string } ) { const trigger = triggerRegistry.get(triggerId); @@ -31,5 +31,4 @@ export function attachAction( if (!trigger.actionIds.find(id => id === actionId)) { trigger.actionIds.push(actionId); } - triggerRegistry.set(trigger.id, trigger); } diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/detach_action.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/detach_action.ts index 7cb4670e50b12..f77bb6b5bb8eb 100644 --- a/src/legacy/core_plugins/embeddable_api/public/triggers/detach_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/detach_action.ts @@ -17,10 +17,10 @@ * under the License. */ -import { IRegistry, Trigger } from '../types'; +import { Trigger } from '../types'; export function detachAction( - triggerRegistry: IRegistry, + triggerRegistry: Map, { triggerId, actionId }: { triggerId: string; actionId: string } ) { const trigger = triggerRegistry.get(triggerId); @@ -29,5 +29,4 @@ export function detachAction( } trigger.actionIds = trigger.actionIds.filter(id => id !== actionId); - triggerRegistry.set(trigger.id, trigger); } diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts index beefff8f75b14..5a95da378b847 100644 --- a/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/execute_trigger_actions.test.ts @@ -55,13 +55,13 @@ class TestAction extends Action { } beforeEach(() => { - triggerRegistry.reset(); - actionRegistry.reset(); + triggerRegistry.clear(); + actionRegistry.clear(); executeFn.mockReset(); }); afterAll(() => { - triggerRegistry.reset(); + triggerRegistry.clear(); }); test('executeTriggerActions executes a single action mapped to a trigger', async () => { diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/index.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/index.ts index 134d3387f1eee..82cecafb417f4 100644 --- a/src/legacy/core_plugins/embeddable_api/public/triggers/index.ts +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/index.ts @@ -22,11 +22,8 @@ export { executeTriggerActions } from './execute_trigger_actions'; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const APPLY_FILTER_TRIGGER = 'FITLER_TRIGGER'; - -import { createRegistry } from '../create_registry'; import { Trigger } from '../types'; - -export const triggerRegistry = createRegistry(); +export const triggerRegistry = new Map(); triggerRegistry.set(CONTEXT_MENU_TRIGGER, { id: CONTEXT_MENU_TRIGGER, diff --git a/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts b/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts index f457b42118887..f2dac33da80b1 100644 --- a/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/triggers/trigger_registry.test.ts @@ -25,11 +25,11 @@ import { attachAction } from './attach_action'; import { detachAction } from './detach_action'; beforeAll(() => { - triggerRegistry.reset(); + triggerRegistry.clear(); }); afterAll(() => { - triggerRegistry.reset(); + triggerRegistry.clear(); }); test('TriggerRegistry adding and getting a new trigger', async () => { diff --git a/src/legacy/core_plugins/embeddable_api/public/types.ts b/src/legacy/core_plugins/embeddable_api/public/types.ts index 521666acb0308..62fcbc82cbdb9 100644 --- a/src/legacy/core_plugins/embeddable_api/public/types.ts +++ b/src/legacy/core_plugins/embeddable_api/public/types.ts @@ -24,15 +24,6 @@ export interface Trigger { actionIds: string[]; } -// TODO: use the official base Registry interface when available -export interface IRegistry { - get(id: string): T | undefined; - length(): number; - set(id: string, item: T): void; - reset(): void; - getAll(): T[]; -} - export interface PropertySpec { displayName: string; accessPath: string; @@ -49,17 +40,3 @@ export enum ViewMode { EDIT = 'edit', VIEW = 'view', } -export interface TimeRange { - to: string; - from: string; -} - -export enum QueryLanguageType { - KUERY = 'kuery', - LUCENE = 'lucene', -} - -export interface Query { - language: QueryLanguageType; - query: string; -} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index a8246288eabc7..f4adcbf5cb7e3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -325,13 +325,7 @@ test('field name change', async () => { // ensure that after async loading is complete the DynamicOptionsSwitch is disabled, because this is not a "string" field expect(component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]')).toHaveLength(0); await update(); - - - /* - The issue with enzyme@3.9.0 -> the fix has not been released yet -> https://github.com/airbnb/enzyme/pull/2027 - TODO: Enable the expectation after the next patch released expect(component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]')).toHaveLength(1); - */ component.setProps({ controlParams diff --git a/src/legacy/core_plugins/interpreter/common/types/kibana_context.ts b/src/legacy/core_plugins/interpreter/common/types/kibana_context.ts index 4cd64d92743b0..89c976611c8f0 100644 --- a/src/legacy/core_plugins/interpreter/common/types/kibana_context.ts +++ b/src/legacy/core_plugins/interpreter/common/types/kibana_context.ts @@ -17,14 +17,16 @@ * under the License. */ -import { Filters, Query, TimeRange } from 'ui/visualize'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Filter } from '@kbn/es-query'; const name = 'kibana_context'; export interface KibanaContext { type: typeof name; query?: Query; - filters?: Filters; + filters?: Filter[]; timeRange?: TimeRange; } diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js index a7ac4b680f327..528edafea7a2f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/area.js @@ -49,6 +49,7 @@ export default function PointSeriesVisType(Private) { }, labels: { show: true, + filter: true, truncate: 100 }, title: {} diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html index 5fb086585a244..5b316bd2ef420 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.html @@ -255,7 +255,33 @@ i18n-default-message="Scale to Data Bounds" >
- + +
+
+ +
+
+ +
+ +
+
+
+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js index 3f97601422d76..e9f839da4ad37 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/point_series/value_axes.js @@ -125,6 +125,12 @@ module.directive('vislibValueAxes', function () { } }; + $scope.updateBoundsMargin = function (axis) { + if (!axis.scale.defaultYExtents) { + delete axis.scale.boundsMargin; + } + }; + $scope.updateAxisName = function (axis) { const axisName = _.capitalize(axis.position) + 'Axis-'; axis.name = axisName + $scope.editorState.params.valueAxes.reduce((value, axis) => { diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js index 1c2e216a6656c..2dd40fe6f8ffd 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/histogram.js @@ -50,6 +50,7 @@ export default function PointSeriesVisType(Private) { }, labels: { show: true, + filter: true, truncate: 100 }, title: {} diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js index 11a598b141241..aa859d9d54219 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/line.js @@ -48,6 +48,7 @@ export default function PointSeriesVisType(Private) { }, labels: { show: true, + filter: true, truncate: 100 }, title: {} diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/quux/quux.js b/src/legacy/core_plugins/kibana/migrations/index.ts similarity index 93% rename from packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/quux/quux.js rename to src/legacy/core_plugins/kibana/migrations/index.ts index fa5897b652843..68c843d2343c8 100644 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/quux/quux.js +++ b/src/legacy/core_plugins/kibana/migrations/index.ts @@ -17,4 +17,5 @@ * under the License. */ -console.log('@elastic/quux'); +// @ts-ignore +export { migrations } from './migrations'; diff --git a/src/legacy/core_plugins/kibana/migrations/is_doc.ts b/src/legacy/core_plugins/kibana/migrations/is_doc.ts new file mode 100644 index 0000000000000..cc50dfa3b2d26 --- /dev/null +++ b/src/legacy/core_plugins/kibana/migrations/is_doc.ts @@ -0,0 +1,31 @@ +/* + * 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 { Doc } from './types'; + +export function isDoc(doc: { [key: string]: unknown } | Doc): doc is Doc { + return ( + typeof doc.id === 'string' && + typeof doc.type === 'string' && + doc.attributes !== null && + typeof doc.attributes === 'object' && + doc.references !== null && + typeof doc.references === 'object' + ); +} diff --git a/src/legacy/core_plugins/kibana/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js similarity index 84% rename from src/legacy/core_plugins/kibana/migrations.js rename to src/legacy/core_plugins/kibana/migrations/migrations.js index 6452b77db1495..ec66e2401e918 100644 --- a/src/legacy/core_plugins/kibana/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.js @@ -18,6 +18,7 @@ */ import { cloneDeep, get, omit, has, flow } from 'lodash'; +import { migrations730 as dashboardMigrations730 } from '../public/dashboard/migrations'; function migrateIndexPattern(doc) { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -113,8 +114,9 @@ const migrateDateHistogramAggregation = doc => { delete agg.params.customInterval; } - if (get(agg, 'params.customBucket.type', null) === 'date_histogram' - && agg.params.customBucket.params + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params ) { if (agg.params.customBucket.params.interval === 'custom') { agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval; @@ -127,7 +129,7 @@ const migrateDateHistogramAggregation = doc => { attributes: { ...doc.attributes, visState: JSON.stringify(visState), - } + }, }; } } @@ -151,7 +153,10 @@ function removeDateHistogramTimeZones(doc) { delete agg.params.time_zone; } - if (get(agg, 'params.customBucket.type', null) === 'date_histogram' && agg.params.customBucket.params) { + if ( + get(agg, 'params.customBucket.type', null) === 'date_histogram' && + agg.params.customBucket.params + ) { delete agg.params.customBucket.params.time_zone; } }); @@ -163,15 +168,16 @@ function removeDateHistogramTimeZones(doc) { // migrate gauge verticalSplit to alignment // https://github.com/elastic/kibana/issues/34636 -function migrateGaugeVerticalSplitToAlignment(doc, logger) { +function migrateGaugeVerticalSplitToAlignment(doc, logger) { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { try { const visState = JSON.parse(visStateJSON); if (visState && visState.type === 'gauge' && !visState.params.gauge.alignment) { - - visState.params.gauge.alignment = visState.params.gauge.verticalSplit ? 'vertical' : 'horizontal'; + visState.params.gauge.alignment = visState.params.gauge.verticalSplit + ? 'vertical' + : 'horizontal'; delete visState.params.gauge.verticalSplit; return { ...doc, @@ -228,7 +234,7 @@ function transformFilterStringToQueryObject(doc) { // migrate the annotations query string: const annotations = get(visState, 'params.annotations') || []; - annotations.forEach((item) => { + annotations.forEach(item => { if (!item.query_string) { // we don't need to transform anything if there isn't a filter at all return; @@ -243,7 +249,7 @@ function transformFilterStringToQueryObject(doc) { }); // migrate the series filters const series = get(visState, 'params.series') || []; - series.forEach((item) => { + series.forEach(item => { if (!item.filter) { // we don't need to transform anything if there isn't a filter at all return; @@ -259,7 +265,7 @@ function transformFilterStringToQueryObject(doc) { // series item split filters filter if (item.split_filters) { const splitFilters = get(item, 'split_filters') || []; - splitFilters.forEach((filter) => { + splitFilters.forEach(filter => { if (!filter.filter) { // we don't need to transform anything if there isn't a filter at all return; @@ -287,10 +293,10 @@ function migrateFiltersAggQuery(doc) { try { const visState = JSON.parse(visStateJSON); if (visState && visState.aggs) { - visState.aggs.forEach((agg) => { + visState.aggs.forEach(agg => { if (agg.type !== 'filters') return; - agg.params.filters.forEach((filter) => { + agg.params.filters.forEach(filter => { if (filter.input.language) return filter; filter.input.language = 'lucene'; }); @@ -311,16 +317,72 @@ function migrateFiltersAggQuery(doc) { return doc; } -const executeMigrations720 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation); -const executeMigrations730 = flow(migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject, migrateFiltersAggQuery); +function replaceMovAvgToMovFn(doc, logger) { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + + if (visState && visState.type === 'metrics') { + const series = get(visState, 'params.series', []); + + series.forEach(part => { + if (part.metrics && Array.isArray(part.metrics)) { + part.metrics.forEach(metric => { + if (metric.type === 'moving_average') { + metric.model_type = metric.model; + metric.alpha = get(metric, 'settings.alpha', 0.3); + metric.beta = get(metric, 'settings.beta', 0.1); + metric.gamma = get(metric, 'settings.gamma', 0.3); + metric.period = get(metric, 'settings.period', 1); + metric.multiplicative = get(metric, 'settings.type') === 'mult'; + + delete metric.minimize; + delete metric.model; + delete metric.settings; + delete metric.predict; + } + }); + } + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + logger.warning(`Exception @ replaceMovAvgToMovFn! ${e}`); + logger.warning(`Exception @ replaceMovAvgToMovFn! Payload: ${visStateJSON}`); + } + } + + return doc; +} + +const executeMigrations720 = flow( + migratePercentileRankAggregation, + migrateDateHistogramAggregation +); +const executeMigrations730 = flow( + migrateGaugeVerticalSplitToAlignment, + transformFilterStringToQueryObject, + migrateFiltersAggQuery, + replaceMovAvgToMovFn +); export const migrations = { 'index-pattern': { - '6.5.0': (doc) => { + '6.5.0': doc => { doc.attributes.type = doc.attributes.type || undefined; doc.attributes.typeMeta = doc.attributes.typeMeta || undefined; return doc; - } + }, }, visualization: { /** @@ -334,7 +396,7 @@ export const migrations = { * only contained the 6.7.2 migration and not the 7.0.1 migration. */ '6.7.2': removeDateHistogramTimeZones, - '7.0.0': (doc) => { + '7.0.0': doc => { // Set new "references" attribute doc.references = doc.references || []; @@ -411,17 +473,20 @@ export const migrations = { newDoc.attributes.visState = JSON.stringify(visState); return newDoc; } catch (e) { - throw new Error(`Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}`); + throw new Error( + `Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}` + ); } }, '7.0.1': removeDateHistogramTimeZones, '7.2.0': doc => executeMigrations720(doc), - '7.3.0': executeMigrations730 + '7.3.0': executeMigrations730, }, dashboard: { - '7.0.0': (doc) => { + '7.0.0': doc => { // Set new "references" attribute doc.references = doc.references || []; + // Migrate index pattern migrateIndexPattern(doc); // Migrate panels @@ -455,9 +520,10 @@ export const migrations = { doc.attributes.panelsJSON = JSON.stringify(panels); return doc; }, + '7.3.0': dashboardMigrations730 }, search: { - '7.0.0': (doc) => { + '7.0.0': doc => { // Set new "references" attribute doc.references = doc.references || []; // Migrate index pattern diff --git a/src/legacy/core_plugins/kibana/migrations.test.js b/src/legacy/core_plugins/kibana/migrations/migrations.test.js similarity index 93% rename from src/legacy/core_plugins/kibana/migrations.test.js rename to src/legacy/core_plugins/kibana/migrations/migrations.test.js index 2b9fd2b03bdd0..12ede94894c73 100644 --- a/src/legacy/core_plugins/kibana/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations/migrations.test.js @@ -989,6 +989,51 @@ Array [ }); }); }); + + describe('replaceMovAvgToMovFn()', () => { + let doc; + + beforeEach(() => { + doc = { + attributes: { + title: 'VIS', + visState: `{"title":"VIS","type":"metrics","params":{"id":"61ca57f0-469d-11e7-af02-69e470af7417", + "type":"timeseries","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(0,156,224,1)", + "split_mode":"terms","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count", + "numerator":"FlightDelay:true"},{"settings":"","minimize":0,"window":5,"model": + "holt_winters","id":"23054fe0-8915-11e9-9b86-d3f94982620f","type":"moving_average","field": + "61ca57f2-469d-11e7-af02-69e470af7417","predict":1}],"separate_axis":0,"axis_position":"right", + "formatter":"number","chart_type":"line","line_width":"2","point_size":"0","fill":0.5,"stacked":"none", + "label":"Percent Delays","terms_size":"2","terms_field":"OriginCityName"}],"time_field":"timestamp", + "index_pattern":"kibana_sample_data_flights","interval":">=12h","axis_position":"left","axis_formatter": + "number","show_legend":1,"show_grid":1,"annotations":[{"fields":"FlightDelay,Cancelled,Carrier", + "template":"{{Carrier}}: Flight Delayed and Cancelled!","index_pattern":"kibana_sample_data_flights", + "query_string":"FlightDelay:true AND Cancelled:true","id":"53b7dff0-4c89-11e8-a66a-6989ad5a0a39", + "color":"rgba(0,98,177,1)","time_field":"timestamp","icon":"fa-exclamation-triangle", + "ignore_global_filters":1,"ignore_panel_filters":1,"hidden":true}],"legend_position":"bottom", + "axis_scale":"normal","default_index_pattern":"kibana_sample_data_flights","default_timefield":"timestamp"}, + "aggs":[]}`, + }, + migrationVersion: { + visualization: '7.2.0', + }, + type: 'visualization', + }; + }); + + test('should add some necessary moving_fn fields', () => { + const migratedDoc = migrate(doc); + const visState = JSON.parse(migratedDoc.attributes.visState); + const metric = visState.params.series[0].metrics[1]; + + expect(metric).toHaveProperty('model_type'); + expect(metric).toHaveProperty('alpha'); + expect(metric).toHaveProperty('beta'); + expect(metric).toHaveProperty('gamma'); + expect(metric).toHaveProperty('period'); + expect(metric).toHaveProperty('multiplicative'); + }); + }); }); describe('7.3.0 tsvb', () => { const migrate = doc => migrations.visualization['7.3.0'](doc); @@ -1019,15 +1064,18 @@ Array [ { filter: 'Filter Bytes Test:>1000', split_filters: [{ filter: 'bytes:>1000' }], - } - ] + }, + ], }; const markdownDoc = generateDoc({ params: markdownParams }); const migratedMarkdownDoc = migrate(markdownDoc); const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series; - expect(markdownSeries[0].filter.query).toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].filter); - expect(markdownSeries[0].split_filters[0].filter.query) - .toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter); + expect(markdownSeries[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].filter + ); + expect(markdownSeries[0].split_filters[0].filter.query).toBe( + JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter + ); }); it('should change series item filters from a string into an object for all filters', () => { const params = { @@ -1037,16 +1085,22 @@ Array [ { filter: 'Filter Bytes Test:>1000', split_filters: [{ filter: 'bytes:>1000' }], - } + }, ], annotations: [{ query_string: 'bytes:>1000' }], }; const timeSeriesDoc = generateDoc({ params: params }); const migratedtimeSeriesDoc = migrate(timeSeriesDoc); const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; - expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual(expect.arrayContaining(['query', 'language'])); - expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual(expect.arrayContaining(['query', 'language'])); - expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual(expect.arrayContaining(['query', 'language'])); + expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual( + expect.arrayContaining(['query', 'language']) + ); + expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual( + expect.arrayContaining(['query', 'language']) + ); }); it('should not fail on a metric visualization without a filter in a series item', () => { const params = { type: 'metric', series: [{}, {}, {}] }; diff --git a/src/legacy/core_plugins/kibana/migrations/types.ts b/src/legacy/core_plugins/kibana/migrations/types.ts new file mode 100644 index 0000000000000..144151ed80d43 --- /dev/null +++ b/src/legacy/core_plugins/kibana/migrations/types.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectReference } from '../../../../legacy/server/saved_objects/routes/types'; + +export interface SavedObjectAttributes { + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; +} + +export interface Doc { + references: SavedObjectReference[]; + attributes: Attributes; + id: string; + type: string; +} + +export interface DocPre700 { + attributes: Attributes; + id: string; + type: string; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts index 0fc74f30a997c..f9f5cfe0214b2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts @@ -40,6 +40,8 @@ export function getSavedDashboardMock( save: () => { return Promise.resolve('123'); }, + getQuery: () => ({ query: '', language: 'kuery' }), + getFilters: () => [], ...config, }; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts b/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts index ef9cfaf05d626..205479032b36e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/actions/view.ts @@ -20,8 +20,10 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import { createAction } from 'redux-actions'; -import { Filters, Query, TimeRange } from 'ui/embeddable'; import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Filter } from '@kbn/es-query'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { KibanaAction } from '../../selectors/types'; import { DashboardViewMode } from '../dashboard_view_mode'; import { PanelId } from '../selectors'; @@ -72,7 +74,7 @@ export interface UpdateRefreshConfigAction extends KibanaAction {} export interface UpdateFiltersAction - extends KibanaAction {} + extends KibanaAction {} export interface UpdateQueryAction extends KibanaAction {} @@ -108,5 +110,5 @@ export const updateTimeRange = createAction(ViewActionTypeKeys.UPDATE export const updateRefreshConfig = createAction( ViewActionTypeKeys.UPDATE_REFRESH_CONFIG ); -export const updateFilters = createAction(ViewActionTypeKeys.UPDATE_FILTERS); +export const updateFilters = createAction(ViewActionTypeKeys.UPDATE_FILTERS); export const updateQuery = createAction(ViewActionTypeKeys.UPDATE_QUERY); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx index 32520e524d751..473f2e0be4bb4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -18,40 +18,27 @@ */ import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import angular from 'angular'; // @ts-ignore import { uiModules } from 'ui/modules'; -import chrome, { IInjector } from 'ui/chrome'; +import { IInjector } from 'ui/chrome'; import { wrapInI18nContext } from 'ui/i18n'; -import { toastNotifications } from 'ui/notify'; // @ts-ignore import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; // @ts-ignore -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; // @ts-ignore import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; -import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; - // @ts-ignore import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter'; // @ts-ignore import { FilterManagerProvider } from 'ui/filter_manager'; -import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; -import { ContextMenuActionsRegistryProvider, Query, EmbeddableFactory } from 'ui/embeddable'; -import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; -import { timefilter } from 'ui/timefilter'; - -import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing/get_unhashable_states_provider'; +import { EmbeddableFactory } from 'ui/embeddable'; import { AppStateClass as TAppStateClass, @@ -63,62 +50,19 @@ import { Filter } from '@kbn/es-query'; import { TimeRange } from 'ui/timefilter/time_history'; import { IndexPattern } from 'ui/index_patterns'; import { IPrivate } from 'ui/private'; -import { StaticIndexPattern } from 'src/legacy/core_plugins/data/public'; -import { SaveOptions } from 'ui/saved_objects/saved_object'; +import { StaticIndexPattern, Query } from 'src/legacy/core_plugins/data/public'; import moment from 'moment'; import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard'; -import { - DashboardAppState, - SavedDashboardPanel, - EmbeddableFactoryRegistry, - NavAction, -} from './types'; +import { DashboardAppState, SavedDashboardPanel, ConfirmModalFn, AddFilterFn } from './types'; // @ts-ignore -- going away soon import { DashboardViewportProvider } from './viewport/dashboard_viewport_provider'; -import { showNewVisModal } from '../visualize/wizard'; -import { showOptionsPopover } from './top_nav/show_options_popover'; -import { showAddPanel } from './top_nav/show_add_panel'; -import { DashboardSaveModal } from './top_nav/save_modal'; -import { showCloneModal } from './top_nav/show_clone_modal'; -import { saveDashboard } from './lib'; import { DashboardStateManager } from './dashboard_state_manager'; -import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -import { getTopNavConfig } from './top_nav/get_top_nav_config'; -import { TopNavIds } from './top_nav/top_nav_ids'; import { DashboardViewMode } from './dashboard_view_mode'; -import { getDashboardTitle } from './dashboard_strings'; -import { panelActionsStore } from './store/panel_actions_store'; - -type ConfirmModalFn = ( - message: string, - confirmOptions: { - onConfirm: () => void; - onCancel: () => void; - confirmButtonText: string; - cancelButtonText: string; - defaultFocusedButton: string; - title: string; - } -) => void; - -type AddFilterFn = ( - { - field, - value, - operator, - index, - }: { - field: string; - value: string; - operator: string; - index: string; - }, - appState: TAppState -) => void; +import { DashboardAppController } from './dashboard_app_controller'; -interface DashboardAppScope extends ng.IScope { +export interface DashboardAppScope extends ng.IScope { dash: SavedObjectDashboard; appState: TAppState; screenTitle: string; @@ -160,535 +104,6 @@ interface DashboardAppScope extends ng.IScope { refresh: () => void; } -class DashboardAppController { - // Part of the exposed plugin API - do not remove without careful consideration. - public appStatus = { - dirty: false, - }; - - constructor({ - $scope, - $rootScope, - $route, - $routeParams, - getAppState, - dashboardConfig, - localStorage, - Private, - kbnUrl, - AppStateClass, - indexPatterns, - config, - confirmModal, - addFilter, - courier, - }: { - courier: { fetch: () => void }; - $scope: DashboardAppScope; - $route: any; - $rootScope: ng.IRootScopeService; - $routeParams: any; - getAppState: { - previouslyStored: () => TAppState | undefined; - }; - indexPatterns: { - getDefault: () => Promise; - }; - dashboardConfig: any; - localStorage: any; - Private: IPrivate; - kbnUrl: KbnUrl; - AppStateClass: TAppStateClass; - config: any; - confirmModal: ( - message: string, - confirmOptions: { - onConfirm: () => void; - onCancel: () => void; - confirmButtonText: string; - cancelButtonText: string; - defaultFocusedButton: string; - title: string; - } - ) => void; - addFilter: AddFilterFn; - }) { - const filterManager = Private(FilterManagerProvider); - const queryFilter = Private(FilterBarQueryFilterProvider); - const docTitle = Private<{ change: (title: string) => void }>(DocTitleProvider); - const embeddableFactories = Private( - EmbeddableFactoriesRegistryProvider - ) as EmbeddableFactoryRegistry; - const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider); - const getUnhashableStates = Private(getUnhashableStatesProvider); - const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); - - // @ts-ignore This code is going away shortly. - panelActionsStore.initializeFromRegistry(panelActionsRegistry); - - const visTypes = Private(VisTypesRegistryProvider); - $scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType]; - - const dash = ($scope.dash = $route.current.locals.dash); - if (dash.id) { - docTitle.change(dash.title); - } - - const dashboardStateManager = new DashboardStateManager({ - savedDashboard: dash, - AppStateClass, - hideWriteControls: dashboardConfig.getHideWriteControls(), - addFilter: ({ field, value, operator, index }) => { - filterActions.addFilter( - field, - value, - operator, - index, - dashboardStateManager.getAppState(), - filterManager - ); - }, - }); - - $scope.getDashboardState = () => dashboardStateManager; - $scope.appState = dashboardStateManager.getAppState(); - - // The 'previouslyStored' check is so we only update the time filter on dashboard open, not during - // normal cross app navigation. - if (dashboardStateManager.getIsTimeSavedWithDashboard() && !getAppState.previouslyStored()) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); - } - - const updateState = () => { - // Following the "best practice" of always have a '.' in your ng-models – - // https://github.com/angular/angular.js/wiki/Understanding-Scopes - $scope.model = { - query: dashboardStateManager.getQuery(), - filters: queryFilter.getFilters(), - timeRestore: dashboardStateManager.getTimeRestore(), - title: dashboardStateManager.getTitle(), - description: dashboardStateManager.getDescription(), - timeRange: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - }; - $scope.panels = dashboardStateManager.getPanels(); - $scope.screenTitle = dashboardStateManager.getTitle(); - - const panelIndexPatterns = dashboardStateManager.getPanelIndexPatterns(); - if (panelIndexPatterns && panelIndexPatterns.length > 0) { - $scope.indexPatterns = panelIndexPatterns; - } else { - indexPatterns.getDefault().then(defaultIndexPattern => { - $scope.$evalAsync(() => { - $scope.indexPatterns = [defaultIndexPattern]; - }); - }); - } - }; - - // Part of the exposed plugin API - do not remove without careful consideration. - this.appStatus = { - dirty: !dash.id, - }; - - dashboardStateManager.registerChangeListener(status => { - this.appStatus.dirty = status.dirty || !dash.id; - updateState(); - }); - - dashboardStateManager.applyFilters( - dashboardStateManager.getQuery() || { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), - }, - queryFilter.getFilters() - ); - - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - updateState(); - - $scope.refresh = () => { - $rootScope.$broadcast('fetch'); - courier.fetch(); - }; - dashboardStateManager.handleTimeChange(timefilter.getTime()); - dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); - $scope.dashboardViewMode = dashboardStateManager.getViewMode(); - - const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; - - const getDashTitle = () => - getDashboardTitle( - dashboardStateManager.getTitle(), - dashboardStateManager.getViewMode(), - dashboardStateManager.getIsDirty(timefilter) - ); - - // Push breadcrumbs to new header navigation - const updateBreadcrumbs = () => { - chrome.breadcrumbs.set([ - { - text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', { - defaultMessage: 'Dashboard', - }), - href: landingPageUrl(), - }, - { text: getDashTitle() }, - ]); - }; - - updateBreadcrumbs(); - dashboardStateManager.registerChangeListener(updateBreadcrumbs); - - $scope.getShouldShowEditHelp = () => - !dashboardStateManager.getPanels().length && - dashboardStateManager.getIsEditMode() && - !dashboardConfig.getHideWriteControls(); - $scope.getShouldShowViewHelp = () => - !dashboardStateManager.getPanels().length && - dashboardStateManager.getIsViewMode() && - !dashboardConfig.getHideWriteControls(); - - $scope.updateQueryAndFetch = function({ query, dateRange }) { - if (dateRange) { - timefilter.setTime(dateRange); - } - - const oldQuery = $scope.model.query; - if (_.isEqual(oldQuery, query)) { - // The user can still request a reload in the query bar, even if the - // query is the same, and in that case, we have to explicitly ask for - // a reload, since no state changes will cause it. - dashboardStateManager.requestReload(); - } else { - $scope.model.query = query; - dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); - } - $scope.refresh(); - }; - - $scope.onRefreshChange = function({ isPaused, refreshInterval }) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value, - }); - }; - - $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); - }; - - $scope.onCancelApplyFilters = () => { - $scope.appState.$newFilters = []; - }; - - $scope.onApplyFilters = filters => { - queryFilter.addFiltersAndChangeTimeFilter(filters); - $scope.appState.$newFilters = []; - }; - - $scope.$watch('appState.$newFilters', (filters: Filter[] = []) => { - if (filters.length === 1) { - $scope.onApplyFilters(filters); - } - }); - - $scope.indexPatterns = []; - - $scope.$watch('model.query', (newQuery: Query) => { - const query = migrateLegacyQuery(newQuery) as Query; - $scope.updateQueryAndFetch({ query }); - }); - - $scope.$listenAndDigestAsync(timefilter, 'fetch', () => { - dashboardStateManager.handleTimeChange(timefilter.getTime()); - // Currently discover relies on this logic to re-fetch. We need to refactor it to rely instead on the - // directly passed down time filter. Then we can get rid of this reliance on scope broadcasts. - $scope.refresh(); - }); - $scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', () => { - dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); - updateState(); - }); - $scope.$listenAndDigestAsync(timefilter, 'timeUpdate', updateState); - - function updateViewMode(newMode: DashboardViewMode) { - $scope.topNavMenu = getTopNavConfig( - newMode, - navActions, - dashboardConfig.getHideWriteControls() - ); // eslint-disable-line no-use-before-define - dashboardStateManager.switchViewMode(newMode); - $scope.dashboardViewMode = newMode; - } - - const onChangeViewMode = (newMode: DashboardViewMode) => { - const isPageRefresh = newMode === dashboardStateManager.getViewMode(); - const isLeavingEditMode = !isPageRefresh && newMode === DashboardViewMode.VIEW; - const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter); - - if (!willLoseChanges) { - updateViewMode(newMode); - return; - } - - function revertChangesAndExitEditMode() { - dashboardStateManager.resetState(); - kbnUrl.change( - dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL - ); - // This is only necessary for new dashboards, which will default to Edit mode. - updateViewMode(DashboardViewMode.VIEW); - - // We need to do a hard reset of the timepicker. appState will not reload like - // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on - // reload will cause it not to sync. - if (dashboardStateManager.getIsTimeSavedWithDashboard()) { - dashboardStateManager.syncTimefilterWithDashboard(timefilter); - } - } - - confirmModal( - i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', { - defaultMessage: `Once you discard your changes, there's no getting them back.`, - }), - { - onConfirm: revertChangesAndExitEditMode, - onCancel: _.noop, - confirmButtonText: i18n.translate( - 'kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', - { defaultMessage: 'Discard changes' } - ), - cancelButtonText: i18n.translate( - 'kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', - { defaultMessage: 'Continue editing' } - ), - defaultFocusedButton: ConfirmationButtonTypes.CANCEL, - title: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', { - defaultMessage: 'Discard changes to dashboard?', - }), - } - ); - }; - - /** - * Saves the dashboard. - * - * @param {object} [saveOptions={}] - * @property {boolean} [saveOptions.confirmOverwrite=false] - If true, attempts to create the source so it - * can confirm an overwrite if a document with the id already exists. - * @property {boolean} [saveOptions.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title - * @property {func} [saveOptions.onTitleDuplicate] - function called if duplicate title exists. - * When not provided, confirm modal will be displayed asking user to confirm or cancel save. - * @return {Promise} - * @resolved {String} - The id of the doc - */ - function save(saveOptions: SaveOptions): Promise<{ id?: string } | { error: Error }> { - return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) - .then(function(id) { - if (id) { - toastNotifications.addSuccess({ - title: i18n.translate('kbn.dashboard.dashboardWasSavedSuccessMessage', { - defaultMessage: `Dashboard '{dashTitle}' was saved`, - values: { dashTitle: dash.title }, - }), - 'data-test-subj': 'saveDashboardSuccess', - }); - - if (dash.id !== $routeParams.id) { - kbnUrl.change(createDashboardEditUrl(dash.id)); - } else { - docTitle.change(dash.lastSavedTitle); - updateViewMode(DashboardViewMode.VIEW); - } - } - return { id }; - }) - .catch(error => { - toastNotifications.addDanger({ - title: i18n.translate('kbn.dashboard.dashboardWasNotSavedDangerMessage', { - defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`, - values: { - dashTitle: dash.title, - errorMessage: error.message, - }, - }), - 'data-test-subj': 'saveDashboardFailure', - }); - return { error }; - }); - } - - $scope.showFilterBar = () => - $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); - - $scope.showAddPanel = () => { - dashboardStateManager.setFullScreenMode(false); - $scope.kbnTopNav.click(TopNavIds.ADD); - }; - $scope.enterEditMode = () => { - dashboardStateManager.setFullScreenMode(false); - $scope.kbnTopNav.click('edit'); - }; - const navActions: { - [key: string]: NavAction; - } = {}; - navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true); - navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.VIEW); - navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.EDIT); - navActions[TopNavIds.SAVE] = () => { - const currentTitle = dashboardStateManager.getTitle(); - const currentDescription = dashboardStateManager.getDescription(); - const currentTimeRestore = dashboardStateManager.getTimeRestore(); - const onSave = ({ - newTitle, - newDescription, - newCopyOnSave, - newTimeRestore, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }: { - newTitle: string; - newDescription: string; - newCopyOnSave: boolean; - newTimeRestore: boolean; - isTitleDuplicateConfirmed: boolean; - onTitleDuplicate: () => void; - }) => { - dashboardStateManager.setTitle(newTitle); - dashboardStateManager.setDescription(newDescription); - dashboardStateManager.savedDashboard.copyOnSave = newCopyOnSave; - dashboardStateManager.setTimeRestore(newTimeRestore); - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }; - return save(saveOptions).then((response: { id?: string } | { error: Error }) => { - // If the save wasn't successful, put the original values back. - if (!(response as { id: string }).id) { - dashboardStateManager.setTitle(currentTitle); - dashboardStateManager.setDescription(currentDescription); - dashboardStateManager.setTimeRestore(currentTimeRestore); - } - return response; - }); - }; - - const dashboardSaveModal = ( - {}} - title={currentTitle} - description={currentDescription} - timeRestore={currentTimeRestore} - showCopyOnSave={dash.id ? true : false} - /> - ); - showSaveModal(dashboardSaveModal); - }; - navActions[TopNavIds.CLONE] = () => { - const currentTitle = dashboardStateManager.getTitle(); - const onClone = ( - newTitle: string, - isTitleDuplicateConfirmed: boolean, - onTitleDuplicate: () => void - ) => { - dashboardStateManager.savedDashboard.copyOnSave = true; - dashboardStateManager.setTitle(newTitle); - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, - }; - return save(saveOptions).then((response: { id?: string } | { error: Error }) => { - // If the save wasn't successful, put the original title back. - if ((response as { error: Error }).error) { - dashboardStateManager.setTitle(currentTitle); - } - return response; - }); - }; - - showCloneModal(onClone, currentTitle); - }; - navActions[TopNavIds.ADD] = () => { - const addNewVis = () => { - showNewVisModal(visTypes, { - editorParams: [DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM], - }); - }; - - showAddPanel(dashboardStateManager.addNewPanel, addNewVis, embeddableFactories); - }; - navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => { - showOptionsPopover({ - anchorElement, - useMargins: dashboardStateManager.getUseMargins(), - onUseMarginsChange: (isChecked: boolean) => { - dashboardStateManager.setUseMargins(isChecked); - }, - hidePanelTitles: dashboardStateManager.getHidePanelTitles(), - onHidePanelTitlesChange: (isChecked: boolean) => { - dashboardStateManager.setHidePanelTitles(isChecked); - }, - }); - }; - navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { - showShareContextMenu({ - anchorElement, - allowEmbed: true, - allowShortUrl: !dashboardConfig.getHideWriteControls(), - getUnhashableStates, - objectId: dash.id, - objectType: 'dashboard', - shareContextMenuExtensions: shareContextMenuExtensions.raw, - sharingData: { - title: dash.title, - }, - isDirty: dashboardStateManager.getIsDirty(), - }); - }; - - updateViewMode(dashboardStateManager.getViewMode()); - - // update root source when filters update - const updateSubscription = queryFilter.getUpdates$().subscribe({ - next: () => { - $scope.model.filters = queryFilter.getFilters(); - dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); - }, - }); - - // update data when filters fire fetch event - - const fetchSubscription = queryFilter.getFetches$().subscribe($scope.refresh); - - $scope.$on('$destroy', () => { - updateSubscription.unsubscribe(); - fetchSubscription.unsubscribe(); - dashboardStateManager.destroy(); - }); - - if ( - $route.current.params && - $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] - ) { - dashboardStateManager.addNewPanel( - $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM], - 'visualization' - ); - - kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); - kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); - } - } -} - const app = uiModules.get('app/dashboard', [ 'elasticsearch', 'ngRoute', @@ -711,20 +126,7 @@ app.directive('dashboardApp', function($injector: IInjector) { const Private = $injector.get('Private'); const filterManager = Private(FilterManagerProvider); - const addFilter = ( - { - field, - value, - operator, - index, - }: { - field: string; - value: string; - operator: string; - index: string; - }, - appState: TAppState - ) => { + const addFilter: AddFilterFn = ({ field, value, operator, index }, appState: TAppState) => { filterActions.addFilter(field, value, operator, index, appState, filterManager); }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx new file mode 100644 index 0000000000000..557c780f6192e --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -0,0 +1,585 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import angular from 'angular'; + +import chrome from 'ui/chrome'; +import { toastNotifications } from 'ui/notify'; + +// @ts-ignore +import { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; +import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; + +import { docTitle } from 'ui/doc_title/doc_title'; + +// @ts-ignore +import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; + +import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; +import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; + +import { EmbeddableFactoriesRegistryProvider } from 'ui/embeddable/embeddable_factories_registry'; +import { ContextMenuActionsRegistryProvider } from 'ui/embeddable'; +import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; +import { timefilter } from 'ui/timefilter'; + +import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing/get_unhashable_states_provider'; + +import { + AppStateClass as TAppStateClass, + AppState as TAppState, +} from 'ui/state_management/app_state'; + +import { KbnUrl } from 'ui/url/kbn_url'; +import { Filter } from '@kbn/es-query'; +import { IndexPattern } from 'ui/index_patterns'; +import { IPrivate } from 'ui/private'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { SaveOptions } from 'ui/saved_objects/saved_object'; +import { + DashboardAppState, + EmbeddableFactoryRegistry, + NavAction, + ConfirmModalFn, + AddFilterFn, +} from './types'; + +import { showNewVisModal } from '../visualize/wizard'; +import { showOptionsPopover } from './top_nav/show_options_popover'; +import { showAddPanel } from './top_nav/show_add_panel'; +import { DashboardSaveModal } from './top_nav/save_modal'; +import { showCloneModal } from './top_nav/show_clone_modal'; +import { saveDashboard } from './lib'; +import { DashboardStateManager } from './dashboard_state_manager'; +import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; +import { getTopNavConfig } from './top_nav/get_top_nav_config'; +import { TopNavIds } from './top_nav/top_nav_ids'; +import { DashboardViewMode } from './dashboard_view_mode'; +import { getDashboardTitle } from './dashboard_strings'; +import { panelActionsStore } from './store/panel_actions_store'; +import { DashboardAppScope } from './dashboard_app'; + +export class DashboardAppController { + // Part of the exposed plugin API - do not remove without careful consideration. + appStatus: { + dirty: boolean; + }; + + constructor({ + $scope, + $rootScope, + $route, + $routeParams, + getAppState, + dashboardConfig, + localStorage, + Private, + kbnUrl, + AppStateClass, + indexPatterns, + config, + confirmModal, + addFilter, + courier, + }: { + courier: { fetch: () => void }; + $scope: DashboardAppScope; + $route: any; + $rootScope: ng.IRootScopeService; + $routeParams: any; + getAppState: { + previouslyStored: () => TAppState | undefined; + }; + indexPatterns: { + getDefault: () => Promise; + }; + dashboardConfig: any; + localStorage: any; + Private: IPrivate; + kbnUrl: KbnUrl; + AppStateClass: TAppStateClass; + config: any; + confirmModal: ConfirmModalFn; + addFilter: AddFilterFn; + }) { + const queryFilter = Private(FilterBarQueryFilterProvider); + const embeddableFactories = Private( + EmbeddableFactoriesRegistryProvider + ) as EmbeddableFactoryRegistry; + const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider); + const getUnhashableStates = Private(getUnhashableStatesProvider); + const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); + + // @ts-ignore This code is going away shortly. + panelActionsStore.initializeFromRegistry(panelActionsRegistry); + + const visTypes = Private(VisTypesRegistryProvider); + $scope.getEmbeddableFactory = panelType => embeddableFactories.byName[panelType]; + + const dash = ($scope.dash = $route.current.locals.dash); + if (dash.id) { + docTitle.change(dash.title); + } + + const dashboardStateManager = new DashboardStateManager({ + savedDashboard: dash, + AppStateClass, + hideWriteControls: dashboardConfig.getHideWriteControls(), + addFilter, + }); + + $scope.getDashboardState = () => dashboardStateManager; + $scope.appState = dashboardStateManager.getAppState(); + + // The 'previouslyStored' check is so we only update the time filter on dashboard open, not during + // normal cross app navigation. + if (dashboardStateManager.getIsTimeSavedWithDashboard() && !getAppState.previouslyStored()) { + dashboardStateManager.syncTimefilterWithDashboard(timefilter); + } + + const updateState = () => { + // Following the "best practice" of always have a '.' in your ng-models – + // https://github.com/angular/angular.js/wiki/Understanding-Scopes + $scope.model = { + query: dashboardStateManager.getQuery(), + filters: queryFilter.getFilters(), + timeRestore: dashboardStateManager.getTimeRestore(), + title: dashboardStateManager.getTitle(), + description: dashboardStateManager.getDescription(), + timeRange: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + }; + $scope.panels = dashboardStateManager.getPanels(); + $scope.screenTitle = dashboardStateManager.getTitle(); + + const panelIndexPatterns = dashboardStateManager.getPanelIndexPatterns(); + if (panelIndexPatterns && panelIndexPatterns.length > 0) { + $scope.indexPatterns = panelIndexPatterns; + } else { + indexPatterns.getDefault().then(defaultIndexPattern => { + $scope.$evalAsync(() => { + $scope.indexPatterns = [defaultIndexPattern]; + }); + }); + } + }; + + // Part of the exposed plugin API - do not remove without careful consideration. + this.appStatus = { + dirty: !dash.id, + }; + + dashboardStateManager.registerChangeListener(status => { + this.appStatus.dirty = status.dirty || !dash.id; + updateState(); + }); + + dashboardStateManager.applyFilters( + dashboardStateManager.getQuery() || { + query: '', + language: + localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + }, + queryFilter.getFilters() + ); + + timefilter.disableTimeRangeSelector(); + timefilter.disableAutoRefreshSelector(); + + updateState(); + + $scope.refresh = () => { + $rootScope.$broadcast('fetch'); + courier.fetch(); + }; + dashboardStateManager.handleTimeChange(timefilter.getTime()); + dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); + + const landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`; + + const getDashTitle = () => + getDashboardTitle( + dashboardStateManager.getTitle(), + dashboardStateManager.getViewMode(), + dashboardStateManager.getIsDirty(timefilter) + ); + + // Push breadcrumbs to new header navigation + const updateBreadcrumbs = () => { + chrome.breadcrumbs.set([ + { + text: i18n.translate('kbn.dashboard.dashboardAppBreadcrumbsTitle', { + defaultMessage: 'Dashboard', + }), + href: landingPageUrl(), + }, + { text: getDashTitle() }, + ]); + }; + + updateBreadcrumbs(); + dashboardStateManager.registerChangeListener(updateBreadcrumbs); + + $scope.getShouldShowEditHelp = () => + !dashboardStateManager.getPanels().length && + dashboardStateManager.getIsEditMode() && + !dashboardConfig.getHideWriteControls(); + $scope.getShouldShowViewHelp = () => + !dashboardStateManager.getPanels().length && + dashboardStateManager.getIsViewMode() && + !dashboardConfig.getHideWriteControls(); + + $scope.updateQueryAndFetch = function({ query, dateRange }) { + if (dateRange) { + timefilter.setTime(dateRange); + } + + const oldQuery = $scope.model.query; + if (_.isEqual(oldQuery, query)) { + // The user can still request a reload in the query bar, even if the + // query is the same, and in that case, we have to explicitly ask for + // a reload, since no state changes will cause it. + dashboardStateManager.requestReload(); + } else { + $scope.model.query = query; + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + } + $scope.refresh(); + }; + + $scope.onRefreshChange = function({ isPaused, refreshInterval }) { + timefilter.setRefreshInterval({ + pause: isPaused, + value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value, + }); + }; + + $scope.onFiltersUpdated = filters => { + // The filters will automatically be set when the queryFilter emits an update event (see below) + queryFilter.setFilters(filters); + }; + + $scope.onCancelApplyFilters = () => { + $scope.appState.$newFilters = []; + }; + + $scope.onApplyFilters = filters => { + queryFilter.addFiltersAndChangeTimeFilter(filters); + $scope.appState.$newFilters = []; + }; + + $scope.$watch('appState.$newFilters', (filters: Filter[] = []) => { + if (filters.length === 1) { + $scope.onApplyFilters(filters); + } + }); + + $scope.indexPatterns = []; + + $scope.$watch('model.query', (newQuery: Query) => { + const query = migrateLegacyQuery(newQuery) as Query; + $scope.updateQueryAndFetch({ query }); + }); + + $scope.$listenAndDigestAsync(timefilter, 'fetch', () => { + dashboardStateManager.handleTimeChange(timefilter.getTime()); + // Currently discover relies on this logic to re-fetch. We need to refactor it to rely instead on the + // directly passed down time filter. Then we can get rid of this reliance on scope broadcasts. + $scope.refresh(); + }); + $scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', () => { + dashboardStateManager.handleRefreshConfigChange(timefilter.getRefreshInterval()); + updateState(); + }); + $scope.$listenAndDigestAsync(timefilter, 'timeUpdate', updateState); + + function updateViewMode(newMode: DashboardViewMode) { + $scope.topNavMenu = getTopNavConfig( + newMode, + navActions, + dashboardConfig.getHideWriteControls() + ); // eslint-disable-line no-use-before-define + dashboardStateManager.switchViewMode(newMode); + } + + const onChangeViewMode = (newMode: DashboardViewMode) => { + const isPageRefresh = newMode === dashboardStateManager.getViewMode(); + const isLeavingEditMode = !isPageRefresh && newMode === DashboardViewMode.VIEW; + const willLoseChanges = isLeavingEditMode && dashboardStateManager.getIsDirty(timefilter); + + if (!willLoseChanges) { + updateViewMode(newMode); + return; + } + + function revertChangesAndExitEditMode() { + dashboardStateManager.resetState(); + kbnUrl.change( + dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL + ); + // This is only necessary for new dashboards, which will default to Edit mode. + updateViewMode(DashboardViewMode.VIEW); + + // We need to do a hard reset of the timepicker. appState will not reload like + // it does on 'open' because it's been saved to the url and the getAppState.previouslyStored() check on + // reload will cause it not to sync. + if (dashboardStateManager.getIsTimeSavedWithDashboard()) { + dashboardStateManager.syncTimefilterWithDashboard(timefilter); + } + } + + confirmModal( + i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesDescription', { + defaultMessage: `Once you discard your changes, there's no getting them back.`, + }), + { + onConfirm: revertChangesAndExitEditMode, + onCancel: _.noop, + confirmButtonText: i18n.translate( + 'kbn.dashboard.changeViewModeConfirmModal.confirmButtonLabel', + { defaultMessage: 'Discard changes' } + ), + cancelButtonText: i18n.translate( + 'kbn.dashboard.changeViewModeConfirmModal.cancelButtonLabel', + { defaultMessage: 'Continue editing' } + ), + defaultFocusedButton: ConfirmationButtonTypes.CANCEL, + title: i18n.translate('kbn.dashboard.changeViewModeConfirmModal.discardChangesTitle', { + defaultMessage: 'Discard changes to dashboard?', + }), + } + ); + }; + + /** + * Saves the dashboard. + * + * @param {object} [saveOptions={}] + * @property {boolean} [saveOptions.confirmOverwrite=false] - If true, attempts to create the source so it + * can confirm an overwrite if a document with the id already exists. + * @property {boolean} [saveOptions.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title + * @property {func} [saveOptions.onTitleDuplicate] - function called if duplicate title exists. + * When not provided, confirm modal will be displayed asking user to confirm or cancel save. + * @return {Promise} + * @resolved {String} - The id of the doc + */ + function save(saveOptions: SaveOptions): Promise<{ id?: string } | { error: Error }> { + return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) + .then(function(id) { + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate('kbn.dashboard.dashboardWasSavedSuccessMessage', { + defaultMessage: `Dashboard '{dashTitle}' was saved`, + values: { dashTitle: dash.title }, + }), + 'data-test-subj': 'saveDashboardSuccess', + }); + + if (dash.id !== $routeParams.id) { + kbnUrl.change(createDashboardEditUrl(dash.id)); + } else { + docTitle.change(dash.lastSavedTitle); + updateViewMode(DashboardViewMode.VIEW); + } + } + return { id }; + }) + .catch(error => { + toastNotifications.addDanger({ + title: i18n.translate('kbn.dashboard.dashboardWasNotSavedDangerMessage', { + defaultMessage: `Dashboard '{dashTitle}' was not saved. Error: {errorMessage}`, + values: { + dashTitle: dash.title, + errorMessage: error.message, + }, + }), + 'data-test-subj': 'saveDashboardFailure', + }); + return { error }; + }); + } + + $scope.showFilterBar = () => + $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); + + $scope.showAddPanel = () => { + dashboardStateManager.setFullScreenMode(false); + $scope.kbnTopNav.click(TopNavIds.ADD); + }; + $scope.enterEditMode = () => { + dashboardStateManager.setFullScreenMode(false); + $scope.kbnTopNav.click('edit'); + }; + const navActions: { + [key: string]: NavAction; + } = {}; + navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true); + navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.VIEW); + navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(DashboardViewMode.EDIT); + navActions[TopNavIds.SAVE] = () => { + const currentTitle = dashboardStateManager.getTitle(); + const currentDescription = dashboardStateManager.getDescription(); + const currentTimeRestore = dashboardStateManager.getTimeRestore(); + const onSave = ({ + newTitle, + newDescription, + newCopyOnSave, + newTimeRestore, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }: { + newTitle: string; + newDescription: string; + newCopyOnSave: boolean; + newTimeRestore: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; + }) => { + dashboardStateManager.setTitle(newTitle); + dashboardStateManager.setDescription(newDescription); + dashboardStateManager.savedDashboard.copyOnSave = newCopyOnSave; + dashboardStateManager.setTimeRestore(newTimeRestore); + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return save(saveOptions).then((response: { id?: string } | { error: Error }) => { + // If the save wasn't successful, put the original values back. + if (!(response as { id: string }).id) { + dashboardStateManager.setTitle(currentTitle); + dashboardStateManager.setDescription(currentDescription); + dashboardStateManager.setTimeRestore(currentTimeRestore); + } + return response; + }); + }; + + const dashboardSaveModal = ( + {}} + title={currentTitle} + description={currentDescription} + timeRestore={currentTimeRestore} + showCopyOnSave={dash.id ? true : false} + /> + ); + showSaveModal(dashboardSaveModal); + }; + navActions[TopNavIds.CLONE] = () => { + const currentTitle = dashboardStateManager.getTitle(); + const onClone = ( + newTitle: string, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: () => void + ) => { + dashboardStateManager.savedDashboard.copyOnSave = true; + dashboardStateManager.setTitle(newTitle); + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return save(saveOptions).then((response: { id?: string } | { error: Error }) => { + // If the save wasn't successful, put the original title back. + if ((response as { error: Error }).error) { + dashboardStateManager.setTitle(currentTitle); + } + return response; + }); + }; + + showCloneModal(onClone, currentTitle); + }; + navActions[TopNavIds.ADD] = () => { + const addNewVis = () => { + showNewVisModal(visTypes, { + editorParams: [DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM], + }); + }; + + showAddPanel(dashboardStateManager.addNewPanel, addNewVis, embeddableFactories); + }; + navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => { + showOptionsPopover({ + anchorElement, + useMargins: dashboardStateManager.getUseMargins(), + onUseMarginsChange: (isChecked: boolean) => { + dashboardStateManager.setUseMargins(isChecked); + }, + hidePanelTitles: dashboardStateManager.getHidePanelTitles(), + onHidePanelTitlesChange: (isChecked: boolean) => { + dashboardStateManager.setHidePanelTitles(isChecked); + }, + }); + }; + navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => { + showShareContextMenu({ + anchorElement, + allowEmbed: true, + allowShortUrl: !dashboardConfig.getHideWriteControls(), + getUnhashableStates, + objectId: dash.id, + objectType: 'dashboard', + shareContextMenuExtensions: shareContextMenuExtensions.raw, + sharingData: { + title: dash.title, + }, + isDirty: dashboardStateManager.getIsDirty(), + }); + }; + + updateViewMode(dashboardStateManager.getViewMode()); + + // update root source when filters update + const updateSubscription = queryFilter.getUpdates$().subscribe({ + next: () => { + $scope.model.filters = queryFilter.getFilters(); + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + }, + }); + + // update data when filters fire fetch event + + const fetchSubscription = queryFilter.getFetches$().subscribe($scope.refresh); + + $scope.$on('$destroy', () => { + updateSubscription.unsubscribe(); + fetchSubscription.unsubscribe(); + dashboardStateManager.destroy(); + }); + + if ( + $route.current.params && + $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM] + ) { + dashboardStateManager.addNewPanel( + $route.current.params[DashboardConstants.NEW_VISUALIZATION_ID_PARAM], + 'visualization' + ); + + kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); + kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); + } + } +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts index 99e6565389bdf..9aa909190d6b2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts @@ -23,11 +23,12 @@ import _ from 'lodash'; import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_monitor_factory'; import { StaticIndexPattern } from 'ui/index_patterns'; import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state'; -import { TimeRange, Query } from 'ui/embeddable'; import { Timefilter } from 'ui/timefilter'; +import { RefreshInterval } from 'ui/timefilter/timefilter'; import { Filter } from '@kbn/es-query'; import moment from 'moment'; -import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { TimeRange } from 'ui/timefilter/time_history'; import { DashboardViewMode } from './dashboard_view_mode'; import { FilterUtils } from './lib/filter_utils'; import { PanelUtils } from './panel/panel_utils'; @@ -72,12 +73,10 @@ import { DashboardAppState, SavedDashboardPanel, SavedDashboardPanelMap, - StagedFilter, DashboardAppStateParameters, + AddFilterFn, } from './types'; -export type AddFilterFuntion = ({ field, value, operator, index }: StagedFilter) => void; - /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the * app. There are two "sources of truth" that need to stay in sync - AppState (aka the `_a` portion of the url) and @@ -99,7 +98,7 @@ export class DashboardStateManager { private changeListeners: Array<(status: { dirty: boolean }) => void>; private stateMonitor: StateMonitor; private panelIndexPatternMapping: { [key: string]: StaticIndexPattern[] } = {}; - private addFilter: AddFilterFuntion; + private addFilter: AddFilterFn; private unsubscribe: () => void; /** @@ -118,7 +117,7 @@ export class DashboardStateManager { savedDashboard: SavedObjectDashboard; AppStateClass: TAppStateClass; hideWriteControls: boolean; - addFilter: AddFilterFuntion; + addFilter: AddFilterFn; }) { this.savedDashboard = savedDashboard; this.hideWriteControls = hideWriteControls; @@ -277,7 +276,7 @@ export class DashboardStateManager { _pushFiltersToStore() { const state = store.getState(); - const dashboardFilters = this.getDashboardFilterBars(); + const dashboardFilters = this.savedDashboard.getFilters(); if ( !_.isEqual( FilterUtils.cleanFiltersForComparison(dashboardFilters), @@ -320,7 +319,7 @@ export class DashboardStateManager { const stagedFilters = getStagedFilters(store.getState()); stagedFilters.forEach(filter => { - this.addFilter(filter); + this.addFilter(filter, this.getAppState()); }); if (stagedFilters.length > 0) { this.saveState(); @@ -385,8 +384,8 @@ export class DashboardStateManager { return { timeTo: this.savedDashboard.timeTo, timeFrom: this.savedDashboard.timeFrom, - filterBars: this.getDashboardFilterBars(), - query: this.getDashboardQuery(), + filterBars: this.savedDashboard.getFilters(), + query: this.savedDashboard.getQuery(), }; } @@ -454,14 +453,6 @@ export class DashboardStateManager { return this.savedDashboard.timeRestore; } - public getDashboardFilterBars() { - return FilterUtils.getFilterBarsForDashboard(this.savedDashboard); - } - - public getDashboardQuery() { - return FilterUtils.getQueryFilterForDashboard(this.savedDashboard); - } - public getLastSavedFilterBars(): Filter[] { return this.lastSavedDashboardFilters.filterBars; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts index eddf8289fabbb..3b6b99dcb6d25 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts @@ -19,9 +19,7 @@ import _ from 'lodash'; import moment, { Moment } from 'moment'; -import { QueryFilter } from 'ui/filter_manager/query_filter'; import { Filter } from '@kbn/es-query'; -import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; /** * @typedef {Object} QueryFilter @@ -30,51 +28,6 @@ import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; */ export class FilterUtils { - /** - * - * @param filter - * @returns {Boolean} True if the filter is of the special query type - * (e.g. goes in the query input bar), false otherwise (e.g. is in the filter bar). - */ - public static isQueryFilter(filter: Filter) { - return filter.query && !filter.meta; - } - - /** - * - * @param {SavedDashboard} dashboard - * @returns {Array.} An array of filters stored with the dashboard. Includes - * both query filters and filter bar filters. - */ - public static getDashboardFilters(dashboard: SavedObjectDashboard): Filter[] { - return dashboard.searchSource.getOwnField('filter'); - } - - /** - * Grabs a saved query to use from the dashboard, or if none exists, creates a default one. - * @param {SavedDashboard} dashboard - * @returns {QueryFilter} - */ - public static getQueryFilterForDashboard(dashboard: SavedObjectDashboard): QueryFilter | string { - if (dashboard.searchSource.getOwnField('query')) { - return dashboard.searchSource.getOwnField('query'); - } - - const dashboardFilters = this.getDashboardFilters(dashboard); - const dashboardQueryFilter = _.find(dashboardFilters, this.isQueryFilter); - return dashboardQueryFilter ? dashboardQueryFilter.query : ''; - } - - /** - * Returns the filters for the dashboard that should appear in the filter bar area. - * @param {SavedDashboard} dashboard - * @return {Array.} Array of filters that should appear in the filter bar for the - * given dashboard - */ - public static getFilterBarsForDashboard(dashboard: SavedObjectDashboard) { - return _.reject(this.getDashboardFilters(dashboard), this.isQueryFilter); - } - /** * Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like * 'now-15m', then it just returns what it was passed). diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts index f8132a07df573..7a38d8d6d1d29 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/get_app_state_defaults.ts @@ -18,7 +18,6 @@ */ import { DashboardViewMode } from '../dashboard_view_mode'; -import { FilterUtils } from './filter_utils'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; import { Pre61SavedDashboardPanel, @@ -37,8 +36,8 @@ export function getAppStateDefaults( timeRestore: savedDashboard.timeRestore, panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [], options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {}, - query: FilterUtils.getQueryFilterForDashboard(savedDashboard), - filters: FilterUtils.getFilterBarsForDashboard(savedDashboard), + query: savedDashboard.getQuery(), + filters: savedDashboard.getFilters(), viewMode: savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT, }; diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/code.js b/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts similarity index 94% rename from packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/code.js rename to src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts index b0d8ed315c941..da2542e854c32 100644 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/baz/code.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts @@ -17,4 +17,4 @@ * under the License. */ -console.log('@elastic/baz'); +export { migrations730 } from './migrations_730'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts new file mode 100644 index 0000000000000..094f60f0a73cf --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DashboardDoc } from './types'; +import { isDoc } from '../../../migrations/is_doc'; + +export function isDashboardDoc( + doc: { [key: string]: unknown } | DashboardDoc +): doc is DashboardDoc { + if (!isDoc(doc)) { + return false; + } + + if (typeof (doc as DashboardDoc).attributes.panelsJSON !== 'string') { + return false; + } + + return true; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts new file mode 100644 index 0000000000000..04ef7b13d8ff1 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { migrations730 } from './migrations_730'; +import { DashboardDoc } from './types'; + +test('dashboard migration 7.3.0 migrates filters to query on search source', () => { + const doc: DashboardDoc = { + id: '1', + type: 'dashboard', + references: [], + attributes: { + description: '', + uiStateJSON: '{}', + version: 1, + timeRestore: false, + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"filter":[{"query":{"query_string":{"query":"n: 6","analyze_wildcard":true}}}],"highlightAll":true,"version":true}', + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const newDoc = migrations730(doc); + + expect(newDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "description": "", + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"filter\\":[],\\"highlightAll\\":true,\\"version\\":true,\\"query\\":{\\"query\\":\\"n: 6\\",\\"language\\":\\"lucene\\"}}", + }, + "panelsJSON": "[{\\"id\\":\\"1\\",\\"type\\":\\"visualization\\",\\"foo\\":true},{\\"id\\":\\"2\\",\\"type\\":\\"visualization\\",\\"bar\\":true}]", + "timeRestore": false, + "uiStateJSON": "{}", + "version": 1, + }, + "id": "1", + "references": Array [], + "type": "dashboard", +} +`); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts new file mode 100644 index 0000000000000..de038cba7385b --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts @@ -0,0 +1,45 @@ +/* + * 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 { DashboardDoc } from './types'; +import { isDashboardDoc } from './is_dashboard_doc'; +import { moveFiltersToQuery } from './move_filters_to_query'; + +export function migrations730( + doc: + | { + [key: string]: unknown; + } + | DashboardDoc +): DashboardDoc | { [key: string]: unknown } { + if (!isDashboardDoc(doc)) { + // NOTE: we should probably throw an error here... but for now following suit and in the + // case of errors, just returning the same document. + return doc; + } + + try { + const searchSource = JSON.parse(doc.attributes.kibanaSavedObjectMeta.searchSourceJSON); + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify( + moveFiltersToQuery(searchSource) + ); + return doc; + } catch (e) { + return doc; + } +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts new file mode 100644 index 0000000000000..1f503ee675407 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; +import { Filter, FilterStateStore } from '@kbn/es-query'; + +const filter: Filter = { + meta: { disabled: false, negate: false, alias: '' }, + query: {}, + $state: { store: FilterStateStore.APP_STATE }, +}; + +const queryFilter: Pre600FilterQuery = { + query: { query_string: { query: 'hi!', analyze_wildcard: true } }, +}; + +test('Migrates an old filter query into the query field', () => { + const newSearchSource = moveFiltersToQuery({ + filter: [filter, queryFilter], + }); + + expect(newSearchSource).toEqual({ + filter: [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: '', + disabled: false, + negate: false, + }, + query: {}, + }, + ], + query: { + language: 'lucene', + query: 'hi!', + }, + }); +}); + +test('Preserves query if search source is new', () => { + const newSearchSource = moveFiltersToQuery({ + filter: [filter], + query: { query: 'bye', language: 'kuery' }, + }); + + expect(newSearchSource.query).toEqual({ query: 'bye', language: 'kuery' }); +}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts new file mode 100644 index 0000000000000..85b200e3b30ee --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts @@ -0,0 +1,69 @@ +/* + * 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 { Query } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; + +export interface Pre600FilterQuery { + // pre 6.0.0 global query:queryString:options were stored per dashboard and would + // be applied even if the setting was subsequently removed from the advanced + // settings. This is considered a bug, and this migration will fix that behavior. + query: { query_string: { query: string } & { [key: string]: unknown } }; +} + +export interface SearchSourcePre600 { + filter: Array; +} + +export interface SearchSource730 { + filter: Filter[]; + query: Query; + highlightAll?: boolean; + version?: boolean; +} + +function isQueryFilter(filter: Filter | { query: unknown }): filter is Pre600FilterQuery { + return filter.query && !(filter as Filter).meta; +} + +export function moveFiltersToQuery( + searchSource: SearchSourcePre600 | SearchSource730 +): SearchSource730 { + const searchSource730: SearchSource730 = { + ...searchSource, + filter: [], + query: (searchSource as SearchSource730).query || { + query: '', + language: 'kuery', + }, + }; + + searchSource.filter.forEach(filter => { + if (isQueryFilter(filter)) { + searchSource730.query = { + query: filter.query.query_string.query, + language: 'lucene', + }; + } else { + searchSource730.filter.push(filter); + } + }); + + return searchSource730; +} diff --git a/src/legacy/core_plugins/embeddable_api/public/create_registry.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/types.ts similarity index 64% rename from src/legacy/core_plugins/embeddable_api/public/create_registry.ts rename to src/legacy/core_plugins/kibana/public/dashboard/migrations/types.ts index dbc8318c78a64..18f038f938bb9 100644 --- a/src/legacy/core_plugins/embeddable_api/public/create_registry.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/types.ts @@ -17,27 +17,22 @@ * under the License. */ -import { IRegistry } from './types'; +import { Doc, DocPre700 } from '../../../migrations/types'; -export const createRegistry = (): IRegistry => { - let data = new Map(); - - const get = (id: string) => data.get(id); - const set = (id: string, obj: T) => { - data.set(id, obj); - }; - const reset = () => { - data = new Map(); +export interface SavedObjectAttributes { + kibanaSavedObjectMeta: { + searchSourceJSON: string; }; - const length = () => data.size; +} - const getAll = () => Array.from(data.values()); +interface DashboardAttributes extends SavedObjectAttributes { + panelsJSON: string; + description: string; + uiStateJSON: string; + version: number; + timeRestore: boolean; +} - return { - get, - set, - reset, - length, - getAll, - }; -}; +export type DashboardDoc = Doc; + +export type DashboardDocPre700 = DocPre700; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.ts b/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.ts index c1fbae7015883..8c1a1585caab7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/reducers/view.ts @@ -20,9 +20,10 @@ import { cloneDeep } from 'lodash'; import { Reducer } from 'redux'; -import { Filters, Query, TimeRange } from 'ui/embeddable'; -import { QueryLanguageType } from 'ui/embeddable/types'; import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Filter } from '@kbn/es-query'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { ViewActions, ViewActionTypeKeys } from '../actions'; import { DashboardViewMode } from '../dashboard_view_mode'; import { PanelId, ViewState } from '../selectors'; @@ -67,7 +68,7 @@ const updateRefreshConfig = (view: ViewState, refreshConfig: RefreshInterval) => refreshConfig, }); -const updateFilters = (view: ViewState, filters: Filters) => ({ +const updateFilters = (view: ViewState, filters: Filter[]) => ({ ...view, filters: cloneDeep(filters), }); @@ -92,7 +93,7 @@ export const viewReducer: Reducer = ( filters: [], hidePanelTitles: false, isFullScreenMode: false, - query: { language: QueryLanguageType.LUCENE, query: '' }, + query: { language: 'lucene', query: '' }, timeRange: { to: 'now', from: 'now-15m' }, refreshConfig: { pause: true, value: 0 }, useMargins: true, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 303e02d63a7f6..1f752833f45a8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -21,6 +21,8 @@ import { SearchSource } from 'ui/courier'; import { SavedObject } from 'ui/saved_objects/saved_object'; import moment from 'moment'; import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; export interface SavedObjectDashboard extends SavedObject { id?: string; @@ -38,4 +40,6 @@ export interface SavedObjectDashboard extends SavedObject { searchSource: SearchSource; destroy: () => void; refreshInterval?: RefreshInterval; + getQuery(): Query; + getFilters(): Filter[]; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js index 06b2920ac28c6..60a4db12ca82c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.js @@ -71,7 +71,6 @@ module.factory('SavedDashboard', function (Private) { clearSavedIndexPattern: true }); - this.showInRecentlyAccessed = true; } @@ -113,5 +112,15 @@ module.factory('SavedDashboard', function (Private) { return `/app/kibana#${createDashboardEditUrl(this.id)}`; }; + SavedDashboard.prototype.getQuery = function () { + return this.searchSource.getOwnField('query') || + { query: '', language: 'kuery' }; + }; + + SavedDashboard.prototype.getFilters = function () { + return this.searchSource.getOwnField('filter') || []; + }; + + return SavedDashboard; }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts index 0fa28366e5834..47d771de41522 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/selectors/dashboard.ts @@ -18,10 +18,12 @@ */ import _ from 'lodash'; -import { ContainerState, EmbeddableMetadata, Query, TimeRange } from 'ui/embeddable'; +import { ContainerState, EmbeddableMetadata } from 'ui/embeddable'; import { EmbeddableCustomization } from 'ui/embeddable/types'; import { Filter } from '@kbn/es-query'; import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { TimeRange } from 'ui/timefilter/time_history'; import { DashboardViewMode } from '../dashboard_view_mode'; import { DashboardMetadata, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts index 8097758d0ef0a..befb6f6936105 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/selectors/types.ts @@ -17,9 +17,11 @@ * under the License. */ -import { EmbeddableMetadata, Query, TimeRange } from 'ui/embeddable'; +import { EmbeddableMetadata } from 'ui/embeddable'; import { Filter } from '@kbn/es-query'; import { RefreshInterval } from 'ui/timefilter/timefilter'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { DashboardViewMode } from '../dashboard_view_mode'; import { SavedDashboardPanelMap } from '../types'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.tsx index bf36c20ceb6e8..27595e2ecd548 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.tsx @@ -37,7 +37,7 @@ import { EuiButton, EuiTitle, } from '@elastic/eui'; -import { SavedObjectAttributes } from 'src/legacy/server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server/saved_objects'; import { EmbeddableFactoryRegistry } from '../types'; interface Props { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/types.ts index 5bb2f783b94a9..0202785ec9a51 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/types.ts @@ -17,10 +17,12 @@ * under the License. */ -import { Query, EmbeddableFactory } from 'ui/embeddable'; +import { EmbeddableFactory } from 'ui/embeddable'; import { AppState } from 'ui/state_management/app_state'; import { UIRegistry } from 'ui/registry/_registry'; import { Filter } from '@kbn/es-query'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { AppState as TAppState } from 'ui/state_management/app_state'; import { DashboardViewMode } from './dashboard_view_mode'; export interface EmbeddableFactoryRegistry extends UIRegistry { @@ -117,3 +119,30 @@ export interface StagedFilter { operator: string; index: string; } + +export type ConfirmModalFn = ( + message: string, + confirmOptions: { + onConfirm: () => void; + onCancel: () => void; + confirmButtonText: string; + cancelButtonText: string; + defaultFocusedButton: string; + title: string; + } +) => void; + +export type AddFilterFn = ( + { + field, + value, + operator, + index, + }: { + field: string; + value: string; + operator: string; + index: string; + }, + appState: TAppState +) => void; diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 112894dce9272..e7afe807bbf5b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -41,7 +41,7 @@ import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/c import { toastNotifications } from 'ui/notify'; import { VisProvider } from 'ui/vis'; import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { intervalOptions } from 'ui/agg_types/buckets/_interval_options'; import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; @@ -196,7 +196,6 @@ function discoverController( const visualizeLoader = Private(VisualizeLoaderProvider); let visualizeHandler; const Vis = Private(VisProvider); - const docTitle = Private(DocTitleProvider); const queryFilter = Private(FilterBarQueryFilterProvider); const responseHandler = vislibSeriesResponseHandlerProvider().handler; const filterManager = Private(FilterManagerProvider); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html index 01cd1ecf769f0..5c56d70698a15 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/details.html @@ -30,7 +30,7 @@ diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html b/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html index ea64a2b181ffc..075468b76090b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/doc_table.html @@ -37,7 +37,7 @@ on-remove-column="onRemoveColumn" > - - [ ]; uiRoutes - .when('/doc/:indexPattern/:index/:type/:id', { + // the old, pre 8.0 route, no longer used, keep it to stay compatible + // somebody might have bookmarked his favorite log messages + .when('/doc/:indexPattern/:index/:type', { template: html, resolve: resolveIndexPattern, k7Breadcrumbs }) - .when('/doc/:indexPattern/:index/:type', { + //the new route, es 7 deprecated types, es 8 removed them + .when('/doc/:indexPattern/:index', { template: html, resolve: resolveIndexPattern, k7Breadcrumbs @@ -73,7 +76,6 @@ app.controller('doc', function ($scope, $route, es) { body: { query: { ids: { - type: $route.current.params.type, values: [$route.current.params.id] } }, diff --git a/src/legacy/core_plugins/kibana/public/doc/index.html b/src/legacy/core_plugins/kibana/public/doc/index.html index 2d7342cddb92f..69f3a6115baee 100644 --- a/src/legacy/core_plugins/kibana/public/doc/index.html +++ b/src/legacy/core_plugins/kibana/public/doc/index.html @@ -55,7 +55,7 @@
- +
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js index 8ac0f31d740c2..ebbbad83ae7dd 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js @@ -19,7 +19,7 @@ import { Field } from 'ui/index_patterns/_field'; import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; import uiRoutes from 'ui/routes'; import { toastNotifications } from 'ui/notify'; @@ -104,7 +104,6 @@ uiRoutes controllerAs: 'fieldSettings', controller: function FieldEditorPageController($scope, $route, $timeout, $http, Private, config) { const getConfig = (...args) => config.get(...args); - const docTitle = Private(DocTitleProvider); const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); const kbnUrl = Private(KbnUrlProvider); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index 3b6045bdc6e11..24e23c747bf5e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import './index_header'; import './create_edit_field'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; import { IndicesEditSectionsProvider } from './edit_sections'; import { fatalError, toastNotifications } from 'ui/notify'; @@ -182,7 +182,6 @@ uiModules.get('apps/management') $scope.indexPatternListProvider = indexPatternListProvider; $scope.indexPattern.tags = indexPatternListProvider.getIndexPatternTags($scope.indexPattern); $scope.getFieldInfo = indexPatternListProvider.getFieldInfo; - const docTitle = Private(DocTitleProvider); docTitle.change($scope.indexPattern.title); const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => { diff --git a/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts b/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts index 388483c5c1c03..85a702cb93296 100644 --- a/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts +++ b/src/legacy/core_plugins/kibana/public/selectors/dashboard_selectors.ts @@ -17,8 +17,9 @@ * under the License. */ -import { Query, TimeRange } from 'ui/embeddable'; import { Filter } from '@kbn/es-query'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { DashboardViewMode } from '../dashboard/dashboard_view_mode'; import * as DashboardSelectors from '../dashboard/selectors'; import { PanelId } from '../dashboard/selectors/types'; diff --git a/src/legacy/core_plugins/kibana/public/store.ts b/src/legacy/core_plugins/kibana/public/store.ts index 47458bc5249fc..91aa57d8035f9 100644 --- a/src/legacy/core_plugins/kibana/public/store.ts +++ b/src/legacy/core_plugins/kibana/public/store.ts @@ -20,7 +20,6 @@ import { applyMiddleware, compose, createStore } from 'redux'; import thunk from 'redux-thunk'; -import { QueryLanguageType } from 'ui/embeddable/types'; import { DashboardViewMode } from './dashboard/dashboard_view_mode'; import { reducers } from './reducers'; @@ -39,7 +38,7 @@ export const store = createStore( filters: [], hidePanelTitles: false, isFullScreenMode: false, - query: { language: QueryLanguageType.LUCENE, query: '' }, + query: { language: 'lucene', query: '' }, timeRange: { from: 'now-15m', to: 'now' }, useMargins: true, viewMode: DashboardViewMode.VIEW, diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index e1ec84aee8754..6938437bdde33 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -32,7 +32,7 @@ import angular from 'angular'; import { FormattedMessage } from '@kbn/i18n/react'; import { toastNotifications } from 'ui/notify'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; import { migrateAppState } from './lib'; @@ -132,7 +132,6 @@ function VisEditor( kbnBaseUrl, localStorage ) { - const docTitle = Private(DocTitleProvider); const queryFilter = Private(FilterBarQueryFilterProvider); const getUnhashableStates = Private(getUnhashableStatesProvider); const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 7529f1e92678d..9c3a2cbe7e407 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -20,7 +20,6 @@ import _ from 'lodash'; import { ContainerState, Embeddable } from 'ui/embeddable'; import { OnEmbeddableStateChanged } from 'ui/embeddable/embeddable_factory'; -import { Filters, Query, TimeRange } from 'ui/embeddable/types'; import { StaticIndexPattern } from 'ui/index_patterns'; import { PersistedState } from 'ui/persisted_state'; import { VisualizeLoader } from 'ui/visualize/loader'; @@ -31,6 +30,9 @@ import { VisualizeUpdateParams, } from 'ui/visualize/loader/types'; import { i18n } from '@kbn/i18n'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; export interface VisualizeEmbeddableConfiguration { onEmbeddableStateChanged: OnEmbeddableStateChanged; @@ -51,7 +53,7 @@ export class VisualizeEmbeddable extends Embeddable { private panelTitle?: string; private timeRange?: TimeRange; private query?: Query; - private filters?: Filters; + private filters?: Filter[]; constructor({ onEmbeddableStateChanged, diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts index e10cfb3cdb6c7..954c0962ba7ef 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.ts @@ -29,12 +29,16 @@ import { OnEmbeddableStateChanged, } from 'ui/embeddable/embeddable_factory'; import { VisTypesRegistry } from 'ui/registry/vis_types'; +import { SavedObjectAttributes } from 'src/core/server'; import { VisualizeEmbeddable } from './visualize_embeddable'; -import { VisualizationAttributes } from '../../../../../server/saved_objects/service/saved_objects_client'; import { SavedVisualizations } from '../types'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { getIndexPattern } from './get_index_pattern'; +export interface VisualizationAttributes extends SavedObjectAttributes { + visState: string; +} + export class VisualizeEmbeddableFactory extends EmbeddableFactory { private savedVisualizations: SavedVisualizations; private config: Legacy.KibanaConfig; diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts index 282730ad0e4db..89a7e2bc01818 100644 --- a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts +++ b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.test.ts @@ -17,10 +17,7 @@ * under the License. */ -import { - SavedObject, - SavedObjectsClient, -} from '../../../../../server/saved_objects/service/saved_objects_client'; +import { SavedObject, SavedObjectsClient } from 'src/core/server'; import { collectReferencesDeep } from './collect_references_deep'; const data = [ diff --git a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.ts b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.ts index 7fbcb02a51619..9d620b69bf2e3 100644 --- a/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.ts +++ b/src/legacy/core_plugins/kibana/server/lib/export/collect_references_deep.ts @@ -17,10 +17,7 @@ * under the License. */ -import { - SavedObject, - SavedObjectsClient, -} from '../../../../../server/saved_objects/service/saved_objects_client'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; const MAX_BULK_GET_SIZE = 10000; @@ -30,7 +27,7 @@ interface ObjectsToCollect { } export async function collectReferencesDeep( - savedObjectClient: SavedObjectsClient, + savedObjectClient: SavedObjectsClientContract, objects: ObjectsToCollect[] ) { let result: SavedObject[] = []; diff --git a/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js b/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js index 18afbb94eee05..f148cd8d39fd9 100644 --- a/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js +++ b/src/legacy/core_plugins/markdown_vis/public/markdown_vis.js @@ -43,7 +43,8 @@ function MarkdownVisProvider() { component: MarkdownVisWrapper, defaults: { fontSize: 12, - openLinksInNewTab: false + openLinksInNewTab: false, + markdown: '', } }, editorConfig: { diff --git a/src/legacy/core_plugins/metrics/common/__snapshots__/model_options.test.js.snap b/src/legacy/core_plugins/metrics/common/__snapshots__/model_options.test.js.snap new file mode 100644 index 0000000000000..0fca2a017b911 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/__snapshots__/model_options.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/common/model_options.js MODEL_TYPES should match a snapshot of constants 1`] = ` +Object { + "UNWEIGHTED": "simple", + "WEIGHTED_EXPONENTIAL": "ewma", + "WEIGHTED_EXPONENTIAL_DOUBLE": "holt", + "WEIGHTED_EXPONENTIAL_TRIPLE": "holt_winters", + "WEIGHTED_LINEAR": "linear", +} +`; diff --git a/src/legacy/core_plugins/metrics/common/model_options.js b/src/legacy/core_plugins/metrics/common/model_options.js new file mode 100644 index 0000000000000..22fe7a0abc842 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/model_options.js @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const MODEL_TYPES = { + UNWEIGHTED: 'simple', + WEIGHTED_EXPONENTIAL: 'ewma', + WEIGHTED_EXPONENTIAL_DOUBLE: 'holt', + WEIGHTED_EXPONENTIAL_TRIPLE: 'holt_winters', + WEIGHTED_LINEAR: 'linear', +}; diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/shouldskip/src/index.js b/src/legacy/core_plugins/metrics/common/model_options.test.js similarity index 74% rename from packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/shouldskip/src/index.js rename to src/legacy/core_plugins/metrics/common/model_options.test.js index 5611f96529819..7d01226bdc040 100644 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/shouldskip/src/index.js +++ b/src/legacy/core_plugins/metrics/common/model_options.test.js @@ -17,8 +17,12 @@ * under the License. */ -import bar from '@elastic/bar'; // eslint-disable-line import/no-unresolved +import { MODEL_TYPES } from './model_options'; -export default function(val) { - return 'test [' + val + '] (' + bar(val) + ')'; -} +describe('src/legacy/core_plugins/metrics/common/model_options.js', () => { + describe('MODEL_TYPES', () => { + test('should match a snapshot of constants', () => { + expect(MODEL_TYPES).toMatchSnapshot(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js b/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js index 5d059c2477edf..f3dbb9266befc 100644 --- a/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js +++ b/src/legacy/core_plugins/metrics/public/components/aggs/moving_average.js @@ -18,13 +18,12 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { Fragment } from 'react'; import { AggRow } from './agg_row'; import { AggSelect } from './agg_select'; import { MetricSelect } from './metric_select'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; -import { createTextHandler } from '../lib/create_text_handler'; import { createNumberHandler } from '../lib/create_number_handler'; import { htmlIdGenerator, @@ -34,69 +33,85 @@ import { EuiComboBox, EuiSpacer, EuiFormRow, - EuiCode, - EuiTextArea, + EuiFieldNumber, } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { MODEL_TYPES } from '../../../common/model_options'; -const MovingAverageAggUi = props => { - const { siblings, intl } = props; - const defaults = { - settings: '', - minimize: 0, - window: '', - model: 'simple', - }; - const model = { ...defaults, ...props.model }; - const handleChange = createChangeHandler(props.onChange, model); - const handleSelectChange = createSelectHandler(handleChange); - const handleTextChange = createTextHandler(handleChange); - const handleNumberChange = createNumberHandler(handleChange); +const DEFAULTS = { + model_type: MODEL_TYPES.UNWEIGHTED, + alpha: 0.3, + beta: 0.1, + gamma: 0.3, + period: 1, + multiplicative: true, + window: 5, +}; + +const shouldShowHint = ({ model_type: type, window, period }) => + type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE && period * 2 > window; + +export const MovingAverageAgg = props => { + const { siblings } = props; + + const model = { ...DEFAULTS, ...props.model }; const modelOptions = [ { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.simpleLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.simpleLabel', { defaultMessage: 'Simple', }), - value: 'simple', + value: MODEL_TYPES.UNWEIGHTED, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.linearLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.linearLabel', { defaultMessage: 'Linear', }), - value: 'linear', + value: MODEL_TYPES.WEIGHTED_LINEAR, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.exponentiallyWeightedLabel', { defaultMessage: 'Exponentially Weighted', }), - value: 'ewma', + value: MODEL_TYPES.WEIGHTED_EXPONENTIAL, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.holtLinearLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.holtLinearLabel', { defaultMessage: 'Holt-Linear', }), - value: 'holt', + value: MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE, }, { - label: intl.formatMessage({ - id: 'tsvb.movingAverage.modelOptions.holtWintersLabel', + label: i18n.translate('tsvb.movingAverage.modelOptions.holtWintersLabel', { defaultMessage: 'Holt-Winters', }), - value: 'holt_winters', + value: MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE, }, ]; - const minimizeOptions = [{ label: 'True', value: 1 }, { label: 'False', value: 0 }]; + + const handleChange = createChangeHandler(props.onChange, model); + const handleSelectChange = createSelectHandler(handleChange); + const handleNumberChange = createNumberHandler(handleChange); + const htmlId = htmlIdGenerator(); - const selectedModelOption = modelOptions.find(option => { - return model.model === option.value; - }); - const selectedMinimizeOption = minimizeOptions.find(option => { - return model.minimize === option.value; - }); + const selectedModelOption = modelOptions.find(({ value }) => model.model_type === value); + + const multiplicativeOptions = [ + { + label: i18n.translate('tsvb.movingAverage.multiplicativeOptions.true', { + defaultMessage: 'True', + }), + value: true, + }, + { + label: i18n.translate('tsvb.movingAverage.multiplicativeOptions.false', { + defaultMessage: 'False', + }), + value: false, + }, + ]; + const selectedMultiplicative = multiplicativeOptions.find( + ({ value }) => model.multiplicative === value + ); return ( { - + {i18n.translate('tsvb.movingAverage.aggregationLabel', { + defaultMessage: 'Aggregation', + })} { } + label={i18n.translate('tsvb.movingAverage.metricLabel', { + defaultMessage: 'Metric', + })} > { } + id={htmlId('model_type')} + label={i18n.translate('tsvb.movingAverage.modelLabel', { + defaultMessage: 'Model', + })} > @@ -162,11 +179,14 @@ const MovingAverageAggUi = props => { + label={i18n.translate('tsvb.movingAverage.windowSizeLabel', { + defaultMessage: 'Window Size', + })} + helpText={ + shouldShowHint(model) && + i18n.translate('tsvb.movingAverage.windowSizeHint', { + defaultMessage: 'Window must always be at least twice the size of your period', + }) } > {/* @@ -181,73 +201,109 @@ const MovingAverageAggUi = props => { /> - - - } - > - - - - - - } - > - {/* - EUITODO: The following input couldn't be converted to EUI because of type mis-match. - Should it be text or number? - */} - - - - + {(model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL || + model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE || + model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE) && ( + + - - - } - helpText={ - - Key=Value }} - /> - - } - > - - - + + { + + + + + + } + {(model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE || + model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE) && ( + + + + + + )} + {model.model_type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE && ( + + + + + + + + + + + + + + + + + + )} + + + )} ); }; -MovingAverageAggUi.propTypes = { +MovingAverageAgg.propTypes = { disableDelete: PropTypes.bool, fields: PropTypes.object, model: PropTypes.object, @@ -258,5 +314,3 @@ MovingAverageAggUi.propTypes = { series: PropTypes.object, siblings: PropTypes.array, }; - -export const MovingAverageAgg = injectI18n(MovingAverageAggUi); diff --git a/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js b/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js index f0694ea19bba9..1ffcb2b2db924 100644 --- a/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js +++ b/src/legacy/core_plugins/metrics/public/kbn_vis_types/index.js @@ -36,7 +36,7 @@ export function MetricsVisProvider(Private) { return VisFactory.createReactVisualization({ name: 'metrics', - title: i18n.translate('tsvb.kbnVisTypes.metricsTitle', { defaultMessage: 'Timeseries' }), + title: i18n.translate('tsvb.kbnVisTypes.metricsTitle', { defaultMessage: 'TSVB' }), description: i18n.translate('tsvb.kbnVisTypes.metricsDescription', { defaultMessage: 'Build time-series using a visual pipeline interface', }), diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js index 475870f7829db..b4425bc8571c1 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/bucket_transform.js @@ -311,77 +311,6 @@ describe('bucketTransform', () => { }); }); - describe('moving_average', () => { - it('returns moving_average agg with defaults', () => { - const metric = { id: '2', type: 'moving_average', field: '1' }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.moving_average; - expect(fn(metric, metrics, '10s')).is.eql({ - moving_avg: { - buckets_path: '1', - model: 'simple', - gap_policy: 'skip', - }, - }); - }); - - it('returns moving_average agg with predict', () => { - const metric = { - id: '2', - type: 'moving_average', - field: '1', - predict: 10, - }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.moving_average; - expect(fn(metric, metrics, '10s')).is.eql({ - moving_avg: { - buckets_path: '1', - model: 'simple', - gap_policy: 'skip', - predict: 10, - }, - }); - }); - - it('returns moving_average agg with options', () => { - const metric = { - id: '2', - type: 'moving_average', - field: '1', - model: 'holt_winters', - window: 10, - minimize: 1, - settings: 'alpha=0.9 beta=0.5', - }; - const metrics = [{ id: '1', type: 'avg', field: 'cpu.pct' }, metric]; - const fn = bucketTransform.moving_average; - expect(fn(metric, metrics, '10s')).is.eql({ - moving_avg: { - buckets_path: '1', - model: 'holt_winters', - gap_policy: 'skip', - window: 10, - minimize: true, - settings: { - alpha: 0.9, - beta: 0.5, - }, - }, - }); - }); - - it('throws error if type is missing', () => { - const run = () => bucketTransform.moving_average({ id: 'test', field: 'cpu.pct' }); - expect(run).to.throw(Error, 'Metric missing type'); - }); - - it('throws error if field is missing', () => { - const run = () => bucketTransform.moving_average({ id: 'test', type: 'moving_average' }); - expect(run).to.throw(Error, 'Metric missing field'); - }); - }); - describe('calculation', () => { it('returns calculation(bucket_script)', () => { const metric = { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap new file mode 100644 index 0000000000000..cb377871cee28 --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/__snapshots__/bucket_transform.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js bucketTransform moving_average should return a moving function aggregation API and match a snapshot 1`] = ` +Object { + "moving_fn": Object { + "buckets_path": "61ca57f2-469d-11e7-af02-69e470af7417", + "script": "if (values.length > 1*2) {MovingFunctions.holtWinters(values, 0.6, 0.3, 0.3, 1, true)}", + "window": 10, + }, +} +`; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js index 3e315934b7ad9..95550e2115efb 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js @@ -17,11 +17,11 @@ * under the License. */ -import { parseSettings } from './parse_settings'; import { getBucketsPath } from './get_buckets_path'; import { parseInterval } from './parse_interval'; import { set, isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { MODEL_SCRIPTS } from './moving_fn_scripts'; function checkMetric(metric, fields) { fields.forEach(field => { @@ -199,21 +199,14 @@ export const bucketTransform = { moving_average: (bucket, metrics) => { checkMetric(bucket, ['type', 'field']); - const body = { - moving_avg: { + + return { + moving_fn: { buckets_path: getBucketsPath(bucket.field, metrics), - model: bucket.model || 'simple', - gap_policy: 'skip', // seems sane + window: bucket.window, + script: MODEL_SCRIPTS[bucket.model_type](bucket), }, }; - if (bucket.gap_policy) body.moving_avg.gap_policy = bucket.gap_policy; - if (bucket.window) body.moving_avg.window = Number(bucket.window); - if (bucket.minimize) body.moving_avg.minimize = Boolean(bucket.minimize); - if (bucket.predict) body.moving_avg.predict = Number(bucket.predict); - if (bucket.settings) { - body.moving_avg.settings = parseSettings(bucket.settings); - } - return body; }, calculation: (bucket, metrics, bucketSize) => { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.test.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.test.js new file mode 100644 index 0000000000000..186baf73a86c7 --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.test.js @@ -0,0 +1,67 @@ +/* + * 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 { bucketTransform } from './bucket_transform'; + +describe('src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/bucket_transform.js', () => { + describe('bucketTransform', () => { + let bucket; + let metrics; + + beforeEach(() => { + bucket = { + model_type: 'holt_winters', + alpha: 0.6, + beta: 0.3, + gamma: 0.3, + period: 1, + multiplicative: true, + window: 10, + field: '61ca57f2-469d-11e7-af02-69e470af7417', + id: 'e815ae00-7881-11e9-9392-cbca66a4cf76', + type: 'moving_average', + }; + metrics = [ + { + id: '61ca57f2-469d-11e7-af02-69e470af7417', + numerator: 'FlightDelay:true', + type: 'count', + }, + { + model_type: 'holt_winters', + alpha: 0.6, + beta: 0.3, + gamma: 0.3, + period: 1, + multiplicative: true, + window: 10, + field: '61ca57f2-469d-11e7-af02-69e470af7417', + id: 'e815ae00-7881-11e9-9392-cbca66a4cf76', + type: 'moving_average', + }, + ]; + }); + + describe('moving_average', () => { + test('should return a moving function aggregation API and match a snapshot', () => { + expect(bucketTransform.moving_average(bucket, metrics)).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js index 83486cad6a071..f2e1a15ce68a3 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js @@ -19,14 +19,20 @@ import { calculateAuto } from './calculate_auto'; import moment from 'moment'; -import { getUnitValue } from './unit_to_seconds'; +import { + getUnitValue, + parseInterval, + convertIntervalToUnit, + ASCENDING_UNIT_ORDER, +} from './unit_to_seconds'; import { INTERVAL_STRING_RE, GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; const calculateBucketData = (timeInterval, capabilities) => { - const intervalString = capabilities + let intervalString = capabilities ? capabilities.getValidTimeInterval(timeInterval) : timeInterval; const intervalStringMatch = intervalString.match(INTERVAL_STRING_RE); + const parsedInterval = parseInterval(intervalString); let bucketSize = Number(intervalStringMatch[1]) * getUnitValue(intervalStringMatch[2]); @@ -35,6 +41,20 @@ const calculateBucketData = (timeInterval, capabilities) => { bucketSize = 1; } + // Check decimal + if (parsedInterval.value % 1 !== 0) { + if (parsedInterval.unit !== 'ms') { + const { value, unit } = convertIntervalToUnit( + intervalString, + ASCENDING_UNIT_ORDER[ASCENDING_UNIT_ORDER.indexOf(parsedInterval.unit) - 1] + ); + + intervalString = value + unit; + } else { + intervalString = '1ms'; + } + } + return { bucketSize, intervalString, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js new file mode 100644 index 0000000000000..32bec6aea93ae --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js @@ -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. + */ + +import { MODEL_TYPES } from '../../../../common/model_options'; + +export const MODEL_SCRIPTS = { + [MODEL_TYPES.UNWEIGHTED]: () => 'MovingFunctions.unweightedAvg(values)', + [MODEL_TYPES.WEIGHTED_EXPONENTIAL]: ({ alpha }) => `MovingFunctions.ewma(values, ${alpha})`, + [MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE]: ({ alpha, beta }) => + `MovingFunctions.holt(values, ${alpha}, ${beta})`, + [MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE]: ({ alpha, beta, gamma, period, multiplicative }) => + `if (values.length > ${period}*2) {MovingFunctions.holtWinters(values, ${alpha}, ${beta}, ${gamma}, ${period}, ${multiplicative})}`, + [MODEL_TYPES.WEIGHTED_LINEAR]: () => 'MovingFunctions.linearWeightedAvg(values)', +}; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.test.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.test.js new file mode 100644 index 0000000000000..858e3d30b8ebc --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.test.js @@ -0,0 +1,73 @@ +/* + * 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 { MODEL_TYPES } from '../../../../common/model_options'; +import { MODEL_SCRIPTS } from './moving_fn_scripts'; + +describe('src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/moving_fn_scripts.js', () => { + describe('MODEL_SCRIPTS', () => { + let bucket; + + beforeEach(() => { + bucket = { + alpha: 0.1, + beta: 0.2, + gamma: 0.3, + period: 5, + multiplicative: true, + }; + }); + + test('should return an expected result of the UNWEIGHTED model type', () => { + expect(MODEL_SCRIPTS[MODEL_TYPES.UNWEIGHTED](bucket)).toBe( + 'MovingFunctions.unweightedAvg(values)' + ); + }); + + test('should return an expected result of the WEIGHTED_LINEAR model type', () => { + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_LINEAR](bucket)).toBe( + 'MovingFunctions.linearWeightedAvg(values)' + ); + }); + + test('should return an expected result of the WEIGHTED_EXPONENTIAL model type', () => { + const { alpha } = bucket; + + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_EXPONENTIAL](bucket)).toBe( + `MovingFunctions.ewma(values, ${alpha})` + ); + }); + + test('should return an expected result of the WEIGHTED_EXPONENTIAL_DOUBLE model type', () => { + const { alpha, beta } = bucket; + + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_EXPONENTIAL_DOUBLE](bucket)).toBe( + `MovingFunctions.holt(values, ${alpha}, ${beta})` + ); + }); + + test('should return an expected result of the WEIGHTED_EXPONENTIAL_TRIPLE model type', () => { + const { alpha, beta, gamma, period, multiplicative } = bucket; + + expect(MODEL_SCRIPTS[MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE](bucket)).toBe( + `if (values.length > ${period}*2) {MovingFunctions.holtWinters(values, ${alpha}, ${beta}, ${gamma}, ${period}, ${multiplicative})}` + ); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js index 133599dbe6fb5..4dd67d60f2328 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/unit_to_seconds.js @@ -19,6 +19,8 @@ import { INTERVAL_STRING_RE } from '../../../../common/interval_regexp'; import { sortBy, isNumber } from 'lodash'; +export const ASCENDING_UNIT_ORDER = ['ms', 's', 'm', 'h', 'd', 'w', 'M', 'y']; + const units = { ms: 0.001, s: 1, diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index a658fcc1755ba..7e80d8b32cf0b 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { capabilities } from 'ui/capabilities'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { notify, fatalError, toastNotifications } from 'ui/notify'; import { timezoneProvider } from 'ui/vis/lib/timezone'; @@ -138,7 +138,6 @@ app.controller('timelion', function ( const savedVisualizations = Private(SavedObjectRegistryProvider).byLoaderPropertiesName.visualizations; const timezone = Private(timezoneProvider)(); - const docTitle = Private(DocTitleProvider); const defaultExpression = '.es(*)'; const savedSheet = $route.current.locals.savedSheet; diff --git a/src/legacy/core_plugins/visualizations/public/index.ts b/src/legacy/core_plugins/visualizations/public/index.ts index 202c2354f89eb..b9a1b02a71e58 100644 --- a/src/legacy/core_plugins/visualizations/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/index.ts @@ -58,11 +58,13 @@ export interface VisualizationsSetup { /** @public types */ export { Vis, + visFactory, + DefaultEditorSize, VisParams, VisProvider, VisState, - VisualizationController, - VisType, + // VisualizationController, + // VisType, VisTypesRegistry, Status, } from './types'; diff --git a/src/legacy/core_plugins/visualizations/public/types/index.ts b/src/legacy/core_plugins/visualizations/public/types/index.ts index a7830a8eb9704..ca8916152f781 100644 --- a/src/legacy/core_plugins/visualizations/public/types/index.ts +++ b/src/legacy/core_plugins/visualizations/public/types/index.ts @@ -19,14 +19,16 @@ export { TypesService, + visFactory, + DefaultEditorSize, // types TypesSetup, Vis, VisParams, VisProvider, VisState, - VisualizationController, - VisType, + // VisualizationController, + // VisType, VisTypesRegistry, Status, } from './types_service'; diff --git a/src/legacy/core_plugins/visualizations/public/types/types_service.ts b/src/legacy/core_plugins/visualizations/public/types/types_service.ts index 82ab0ceb00baf..f7f0d2ed155f7 100644 --- a/src/legacy/core_plugins/visualizations/public/types/types_service.ts +++ b/src/legacy/core_plugins/visualizations/public/types/types_service.ts @@ -22,7 +22,9 @@ import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; // @ts-ignore import { VisProvider as Vis } from 'ui/vis/index.js'; // @ts-ignore -import { VisFactoryProvider as VisFactory } from 'ui/vis/vis_factory'; +import { VisFactoryProvider, visFactory } from 'ui/vis/vis_factory'; +// @ts-ignore +import { DefaultEditorSize } from 'ui/vis/editor_size'; import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; /** @@ -34,7 +36,7 @@ export class TypesService { public setup() { return { Vis, - VisFactory, + VisFactoryProvider, VisTypesRegistryProvider, defaultFeedbackMessage, // make default in base vis type, or move? }; @@ -48,12 +50,14 @@ export class TypesService { /** @public */ export type TypesSetup = ReturnType; +export { visFactory, DefaultEditorSize }; + /** @public types */ import * as types from 'ui/vis/vis'; export type Vis = types.Vis; export type VisParams = types.VisParams; export type VisProvider = types.VisProvider; export type VisState = types.VisState; -export { VisualizationController, VisType } from 'ui/vis/vis_types/vis_type'; +// todo: this breaks it // export { VisualizationController, VisType } from 'ui/vis/vis_types/vis_type'; export { VisTypesRegistry } from 'ui/registry/vis_types'; export { Status } from 'ui/vis/update_status'; diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index d7cc1aff7fa1e..eb772e9970ed4 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -19,7 +19,9 @@ import { Server } from '../server/kbn_server'; import { Capabilities } from '../../core/public'; -import { SavedObjectsSchemaDefinition } from '../server/saved_objects/schema'; +// Disable lint errors for imports from src/core/* until SavedObjects migration is complete +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SavedObjectsSchemaDefinition } from '../../core/server/saved_objects/schema'; /** * Usage diff --git a/src/legacy/server/http/index.js b/src/legacy/server/http/index.js index 0b9c660742209..40ac2baa032d6 100644 --- a/src/legacy/server/http/index.js +++ b/src/legacy/server/http/index.js @@ -21,14 +21,14 @@ import { format } from 'url'; import { resolve } from 'path'; import _ from 'lodash'; import Boom from 'boom'; -import Hapi from 'hapi'; + import { setupVersionCheck } from './version_check'; import { registerHapiPlugins } from './register_hapi_plugins'; import { setupBasePathProvider } from './setup_base_path_provider'; import { setupXsrf } from './xsrf'; export default async function (kbnServer, server, config) { - kbnServer.server = new Hapi.Server(kbnServer.newPlatform.params.serverOptions); + kbnServer.server = kbnServer.newPlatform.setup.core.http.server; server = kbnServer.server; setupBasePathProvider(kbnServer); diff --git a/src/legacy/server/http/setup_base_path_provider.js b/src/legacy/server/http/setup_base_path_provider.js index c4873cb8da8b8..6949d7e2eebd0 100644 --- a/src/legacy/server/http/setup_base_path_provider.js +++ b/src/legacy/server/http/setup_base_path_provider.js @@ -18,11 +18,6 @@ */ export function setupBasePathProvider(kbnServer) { - kbnServer.server.decorate('request', 'setBasePath', function (basePath) { - const request = this; - kbnServer.newPlatform.setup.core.http.basePath.set(request, basePath); - }); - kbnServer.server.decorate('request', 'getBasePath', function () { const request = this; return kbnServer.newPlatform.setup.core.http.basePath.get(request); diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index c86b967019b0a..eba6a16674705 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -20,23 +20,22 @@ import { ResponseObject, Server } from 'hapi'; import { - ElasticsearchServiceSetup, ConfigService, - LoggerFactory, + ElasticsearchServiceSetup, InternalCoreSetup, InternalCoreStart, + LoggerFactory, + SavedObjectsClientContract, + SavedObjectsService, } from '../../core/server'; +// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SavedObjectsManagement } from '../../core/server/saved_objects/management'; import { ApmOssPlugin } from '../core_plugins/apm_oss'; import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; import { CapabilitiesModifier } from './capabilities'; import { IndexPatternsServiceFactory } from './index_patterns'; -import { - SavedObjectsClient, - SavedObjectsService, - SavedObjectsSchema, - SavedObjectsManagement, -} from './saved_objects'; import { Capabilities } from '../../core/public'; export interface KibanaConfig { @@ -75,7 +74,7 @@ declare module 'hapi' { } interface Request { - getSavedObjectsClient(): SavedObjectsClient; + getSavedObjectsClient(): SavedObjectsClientContract; getBasePath(): string; getUiSettingsService(): any; getCapabilities(): Promise; @@ -104,7 +103,6 @@ export default class KbnServer { }; stop: null; params: { - serverOptions: ElasticsearchServiceSetup; handledConfigPaths: Unpromise>; }; }; @@ -127,4 +125,4 @@ export { Server, Request, ResponseToolkit } from 'hapi'; // Re-export commonly accessed api types. export { IndexPatternsService } from './index_patterns'; -export { SavedObject, SavedObjectsClient, SavedObjectsService } from './saved_objects'; +export { SavedObjectsService, SavedObjectsClient } from 'src/core/server'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 78a8829dbb08a..72ac219b0413b 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -55,7 +55,7 @@ export default class KbnServer { this.rootDir = rootDir; this.settings = settings || {}; - const { setupDeps, startDeps, serverOptions, handledConfigPaths, logger } = core; + const { setupDeps, startDeps, handledConfigPaths, logger } = core; this.newPlatform = { coreContext: { logger, @@ -64,7 +64,6 @@ export default class KbnServer { start: startDeps, stop: null, params: { - serverOptions, handledConfigPaths, }, }; diff --git a/src/legacy/server/saved_objects/routes/bulk_create.test.ts b/src/legacy/server/saved_objects/routes/bulk_create.test.ts index f981b0a62f605..1e041bb28f75f 100644 --- a/src/legacy/server/saved_objects/routes/bulk_create.test.ts +++ b/src/legacy/server/saved_objects/routes/bulk_create.test.ts @@ -20,7 +20,9 @@ import Hapi from 'hapi'; import { createMockServer } from './_mock_server'; import { createBulkCreateRoute } from './bulk_create'; -import { SavedObjectsClientMock } from '../service/saved_objects_client.mock'; +// Disable lint errors for imports from src/core/* until SavedObjects migration is complete +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SavedObjectsClientMock } from '../../../../core/server/saved_objects/service/saved_objects_client.mock'; describe('POST /api/saved_objects/_bulk_create', () => { let server: Hapi.Server; diff --git a/src/legacy/server/saved_objects/routes/bulk_create.ts b/src/legacy/server/saved_objects/routes/bulk_create.ts index 35b90ecbcd853..b185650494f94 100644 --- a/src/legacy/server/saved_objects/routes/bulk_create.ts +++ b/src/legacy/server/saved_objects/routes/bulk_create.ts @@ -19,7 +19,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectAttributes, SavedObjectsClientContract } from '../'; +import { SavedObjectAttributes, SavedObjectsClientContract } from 'src/core/server'; import { Prerequisites, SavedObjectReference, WithoutQueryAndParams } from './types'; interface SavedObject { diff --git a/src/legacy/server/saved_objects/routes/bulk_get.ts b/src/legacy/server/saved_objects/routes/bulk_get.ts index c3cb3ab4da400..e9eca8e557982 100644 --- a/src/legacy/server/saved_objects/routes/bulk_get.ts +++ b/src/legacy/server/saved_objects/routes/bulk_get.ts @@ -19,12 +19,12 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectsClient } from '../'; +import { SavedObjectsClientContract } from 'src/core/server'; import { Prerequisites } from './types'; interface BulkGetRequest extends Hapi.Request { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; payload: Array<{ type: string; diff --git a/src/legacy/server/saved_objects/routes/create.ts b/src/legacy/server/saved_objects/routes/create.ts index cd5c9af098864..a3f4a926972ca 100644 --- a/src/legacy/server/saved_objects/routes/create.ts +++ b/src/legacy/server/saved_objects/routes/create.ts @@ -19,7 +19,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectAttributes, SavedObjectsClient } from '../'; +import { SavedObjectAttributes, SavedObjectsClient } from 'src/core/server'; import { Prerequisites, SavedObjectReference, WithoutQueryAndParams } from './types'; interface CreateRequest extends WithoutQueryAndParams { diff --git a/src/legacy/server/saved_objects/routes/delete.ts b/src/legacy/server/saved_objects/routes/delete.ts index f5398fd1638df..a718f26bc2014 100644 --- a/src/legacy/server/saved_objects/routes/delete.ts +++ b/src/legacy/server/saved_objects/routes/delete.ts @@ -19,12 +19,12 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectsClient } from '../'; +import { SavedObjectsClientContract } from 'src/core/server'; import { Prerequisites } from './types'; interface DeleteRequest extends Hapi.Request { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; params: { type: string; diff --git a/src/legacy/server/saved_objects/routes/export.test.ts b/src/legacy/server/saved_objects/routes/export.test.ts index 8096bce269bf2..c74548ab1bbd3 100644 --- a/src/legacy/server/saved_objects/routes/export.test.ts +++ b/src/legacy/server/saved_objects/routes/export.test.ts @@ -17,12 +17,14 @@ * under the License. */ -jest.mock('../export', () => ({ +jest.mock('../../../../core/server/saved_objects/export', () => ({ getSortedObjectsForExport: jest.fn(), })); import Hapi from 'hapi'; -import * as exportMock from '../export'; +// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import * as exportMock from '../../../../core/server/saved_objects/export'; import { createMockServer } from './_mock_server'; import { createExportRoute } from './export'; diff --git a/src/legacy/server/saved_objects/routes/export.ts b/src/legacy/server/saved_objects/routes/export.ts index 4b41c8b58c574..4bc5d04b5585a 100644 --- a/src/legacy/server/saved_objects/routes/export.ts +++ b/src/legacy/server/saved_objects/routes/export.ts @@ -20,13 +20,15 @@ import Hapi from 'hapi'; import Joi from 'joi'; import stringify from 'json-stable-stringify'; -import { SavedObjectsClient } from '../'; -import { getSortedObjectsForExport } from '../export'; +import { SavedObjectsClientContract } from 'src/core/server'; +// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getSortedObjectsForExport } from '../../../../core/server/saved_objects/export'; import { Prerequisites } from './types'; interface ExportRequest extends Hapi.Request { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; payload: { type?: string[]; diff --git a/src/legacy/server/saved_objects/routes/find.ts b/src/legacy/server/saved_objects/routes/find.ts index 0d7661af5c171..bb8fb21aea29c 100644 --- a/src/legacy/server/saved_objects/routes/find.ts +++ b/src/legacy/server/saved_objects/routes/find.ts @@ -19,12 +19,12 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectsClient } from '../'; +import { SavedObjectsClientContract } from 'src/core/server'; import { Prerequisites, WithoutQueryAndParams } from './types'; interface FindRequest extends WithoutQueryAndParams { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; query: { per_page: number; diff --git a/src/legacy/server/saved_objects/routes/get.ts b/src/legacy/server/saved_objects/routes/get.ts index db4ab6374402a..4dbb06d53425a 100644 --- a/src/legacy/server/saved_objects/routes/get.ts +++ b/src/legacy/server/saved_objects/routes/get.ts @@ -19,12 +19,12 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectsClient } from '../'; +import { SavedObjectsClientContract } from 'src/core/server'; import { Prerequisites } from './types'; interface GetRequest extends Hapi.Request { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; params: { type: string; diff --git a/src/legacy/server/saved_objects/routes/import.ts b/src/legacy/server/saved_objects/routes/import.ts index 5fccee1614028..ea83328231718 100644 --- a/src/legacy/server/saved_objects/routes/import.ts +++ b/src/legacy/server/saved_objects/routes/import.ts @@ -22,8 +22,10 @@ import Hapi from 'hapi'; import Joi from 'joi'; import { extname } from 'path'; import { Readable } from 'stream'; -import { SavedObjectsClient } from '../'; -import { importSavedObjects } from '../import'; +import { SavedObjectsClientContract } from 'src/core/server'; +// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { importSavedObjects } from '../../../../core/server/saved_objects/import'; import { Prerequisites, WithoutQueryAndParams } from './types'; interface HapiReadableStream extends Readable { @@ -34,7 +36,7 @@ interface HapiReadableStream extends Readable { interface ImportRequest extends WithoutQueryAndParams { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; query: { overwrite: boolean; diff --git a/src/legacy/server/saved_objects/routes/resolve_import_errors.ts b/src/legacy/server/saved_objects/routes/resolve_import_errors.ts index 516a80988ce05..24390df9b6ecb 100644 --- a/src/legacy/server/saved_objects/routes/resolve_import_errors.ts +++ b/src/legacy/server/saved_objects/routes/resolve_import_errors.ts @@ -22,8 +22,10 @@ import Hapi from 'hapi'; import Joi from 'joi'; import { extname } from 'path'; import { Readable } from 'stream'; -import { SavedObjectsClient } from '../'; -import { resolveImportErrors } from '../import'; +import { SavedObjectsClientContract } from 'src/core/server'; +// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { resolveImportErrors } from '../../../../core/server/saved_objects/import'; import { Prerequisites } from './types'; interface HapiReadableStream extends Readable { @@ -34,7 +36,7 @@ interface HapiReadableStream extends Readable { interface ImportRequest extends Hapi.Request { pre: { - savedObjectsClient: SavedObjectsClient; + savedObjectsClient: SavedObjectsClientContract; }; payload: { file: HapiReadableStream; diff --git a/src/legacy/server/saved_objects/routes/types.ts b/src/legacy/server/saved_objects/routes/types.ts index f658a61117890..b3f294b66499b 100644 --- a/src/legacy/server/saved_objects/routes/types.ts +++ b/src/legacy/server/saved_objects/routes/types.ts @@ -18,7 +18,7 @@ */ import Hapi from 'hapi'; -import { SavedObjectsClientContract } from '../'; +import { SavedObjectsClientContract } from 'src/core/server'; export interface SavedObjectReference { name: string; diff --git a/src/legacy/server/saved_objects/routes/update.ts b/src/legacy/server/saved_objects/routes/update.ts index 9d0cde0816dfe..7782253f9b542 100644 --- a/src/legacy/server/saved_objects/routes/update.ts +++ b/src/legacy/server/saved_objects/routes/update.ts @@ -19,7 +19,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; -import { SavedObjectAttributes, SavedObjectsClient } from '../'; +import { SavedObjectAttributes, SavedObjectsClient } from 'src/core/server'; import { Prerequisites, SavedObjectReference } from './types'; interface UpdateRequest extends Hapi.Request { diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 0ab126abdd995..52845abbf08c2 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -17,16 +17,19 @@ * under the License. */ -import { KibanaMigrator } from './migrations'; -import { SavedObjectsSchema } from './schema'; -import { SavedObjectsSerializer } from './serialization'; +// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; +import { SavedObjectsSchema } from '../../../core/server/saved_objects/schema'; +import { SavedObjectsSerializer } from '../../../core/server/saved_objects/serialization'; import { SavedObjectsClient, SavedObjectsRepository, ScopedSavedObjectsClientProvider, -} from './service'; -import { getRootPropertiesObjects } from '../mappings'; -import { SavedObjectsManagement } from './management'; +} from '../../../core/server/saved_objects/service'; +import { getRootPropertiesObjects } from '../../../core/server/saved_objects/mappings'; +import { SavedObjectsManagement } from '../../../core/server/saved_objects/management'; import { createBulkCreateRoute, diff --git a/src/legacy/server/saved_objects/service/lib/errors.ts b/src/legacy/server/saved_objects/service/lib/errors.ts deleted file mode 100644 index e1df155022b11..0000000000000 --- a/src/legacy/server/saved_objects/service/lib/errors.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Boom from 'boom'; - -const code = Symbol('SavedObjectsClientErrorCode'); - -interface DecoratedError extends Boom { - [code]?: string; -} - -function decorate( - error: Error | DecoratedError, - errorCode: string, - statusCode: number, - message?: string -): DecoratedError { - if (isSavedObjectsClientError(error)) { - return error; - } - - const boom = Boom.boomify(error, { - statusCode, - message, - override: false, - }) as DecoratedError; - - boom[code] = errorCode; - - return boom; -} - -export function isSavedObjectsClientError(error: any): error is DecoratedError { - return Boolean(error && error[code]); -} - -// 400 - badRequest -const CODE_BAD_REQUEST = 'SavedObjectsClient/badRequest'; -export function decorateBadRequestError(error: Error, reason?: string) { - return decorate(error, CODE_BAD_REQUEST, 400, reason); -} -export function createBadRequestError(reason?: string) { - return decorateBadRequestError(new Error('Bad Request'), reason); -} -export function createUnsupportedTypeError(type: string) { - return createBadRequestError(`Unsupported saved object type: '${type}'`); -} -export function isBadRequestError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_BAD_REQUEST; -} - -// 400 - invalid version -const CODE_INVALID_VERSION = 'SavedObjectsClient/invalidVersion'; -export function createInvalidVersionError(versionInput?: string) { - return decorate(Boom.badRequest(`Invalid version [${versionInput}]`), CODE_INVALID_VERSION, 400); -} -export function isInvalidVersionError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_INVALID_VERSION; -} - -// 401 - Not Authorized -const CODE_NOT_AUTHORIZED = 'SavedObjectsClient/notAuthorized'; -export function decorateNotAuthorizedError(error: Error, reason?: string) { - return decorate(error, CODE_NOT_AUTHORIZED, 401, reason); -} -export function isNotAuthorizedError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_NOT_AUTHORIZED; -} - -// 403 - Forbidden -const CODE_FORBIDDEN = 'SavedObjectsClient/forbidden'; -export function decorateForbiddenError(error: Error, reason?: string) { - return decorate(error, CODE_FORBIDDEN, 403, reason); -} -export function isForbiddenError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_FORBIDDEN; -} - -// 413 - Request Entity Too Large -const CODE_REQUEST_ENTITY_TOO_LARGE = 'SavedObjectsClient/requestEntityTooLarge'; -export function decorateRequestEntityTooLargeError(error: Error, reason?: string) { - return decorate(error, CODE_REQUEST_ENTITY_TOO_LARGE, 413, reason); -} -export function isRequestEntityTooLargeError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_REQUEST_ENTITY_TOO_LARGE; -} - -// 404 - Not Found -const CODE_NOT_FOUND = 'SavedObjectsClient/notFound'; -export function createGenericNotFoundError(type: string | null = null, id: string | null = null) { - if (type && id) { - return decorate(Boom.notFound(`Saved object [${type}/${id}] not found`), CODE_NOT_FOUND, 404); - } - return decorate(Boom.notFound(), CODE_NOT_FOUND, 404); -} -export function isNotFoundError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_NOT_FOUND; -} - -// 409 - Conflict -const CODE_CONFLICT = 'SavedObjectsClient/conflict'; -export function decorateConflictError(error: Error, reason?: string) { - return decorate(error, CODE_CONFLICT, 409, reason); -} -export function isConflictError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_CONFLICT; -} - -// 503 - Es Unavailable -const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; -export function decorateEsUnavailableError(error: Error, reason?: string) { - return decorate(error, CODE_ES_UNAVAILABLE, 503, reason); -} -export function isEsUnavailableError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_ES_UNAVAILABLE; -} - -// 503 - Unable to automatically create index because of action.auto_create_index setting -const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex'; -export function createEsAutoCreateIndexError() { - const error = Boom.serverUnavailable('Automatic index creation failed'); - error.output.payload.attributes = error.output.payload.attributes || {}; - error.output.payload.attributes.code = 'ES_AUTO_CREATE_INDEX_ERROR'; - - return decorate(error, CODE_ES_AUTO_CREATE_INDEX_ERROR, 503); -} -export function isEsAutoCreateIndexError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR; -} - -// 500 - General Error -const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError'; -export function decorateGeneralError(error: Error, reason?: string) { - return decorate(error, CODE_GENERAL_ERROR, 500, reason); -} diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js index 837a062868ee7..033aeb92926a5 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js +++ b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import { shortUrlLookupProvider } from './short_url_lookup'; -import { SavedObjectsClient } from '../../../saved_objects'; +import { SavedObjectsClient } from '../../../../../core/server'; describe('shortUrlLookupProvider', () => { const ID = 'bf00ad16941fc51420f91a93428b27a0'; diff --git a/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx b/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx index 0d6277efac317..96a9000c5d6cd 100644 --- a/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx +++ b/src/legacy/ui/public/agg_types/controls/components/from_to_list.tsx @@ -86,6 +86,7 @@ function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { defaultMessage: 'IP range from: {value}', values: { value: item.from.value || '*' }, })} + compressed isInvalid={showValidation ? item.from.isInvalid : false} placeholder="*" onChange={ev => { @@ -104,6 +105,7 @@ function FromToList({ showValidation, onBlur, ...rest }: FromToListProps) { defaultMessage: 'IP range to: {value}', values: { value: item.to.value || '*' }, })} + compressed isInvalid={showValidation ? item.to.isInvalid : false} placeholder="*" onChange={ev => { diff --git a/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx b/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx index efb5ca7643c93..769ed9ca28193 100644 --- a/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx +++ b/src/legacy/ui/public/agg_types/controls/components/mask_list.tsx @@ -80,6 +80,7 @@ function MaskList({ showValidation, onBlur, ...rest }: MaskListProps) { defaultMessage: 'CIDR mask: {mask}', values: { mask: mask.value || '*' }, })} + compressed isInvalid={showValidation ? mask.isInvalid : false} placeholder="*" onChange={ev => { diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/_header_global_nav.scss b/src/legacy/ui/public/chrome/directives/header_global_nav/_header_global_nav.scss index 96d6cf52a0c9c..2b841eca6e01d 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/_header_global_nav.scss +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/_header_global_nav.scss @@ -15,6 +15,7 @@ // SASSTODO: Find an actual solution .euiFlyout { top: $euiHeaderChildSize; + height: calc(100% - #{$euiHeaderChildSize}); } } diff --git a/src/legacy/ui/public/doc_title/__tests__/doc_title.js b/src/legacy/ui/public/doc_title/__tests__/doc_title.js index 2c0ec6766cb14..b4c3700e36f68 100644 --- a/src/legacy/ui/public/doc_title/__tests__/doc_title.js +++ b/src/legacy/ui/public/doc_title/__tests__/doc_title.js @@ -17,39 +17,31 @@ * under the License. */ -import _ from 'lodash'; import sinon from 'sinon'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { DocTitleProvider } from '..'; +import { setBaseTitle, docTitle } from '../doc_title'; describe('docTitle Service', function () { let initialDocTitle; const MAIN_TITLE = 'Kibana 4'; - - let docTitle; let $rootScope; beforeEach(function () { initialDocTitle = document.title; document.title = MAIN_TITLE; + setBaseTitle(MAIN_TITLE); }); afterEach(function () { document.title = initialDocTitle; + setBaseTitle(initialDocTitle); }); beforeEach(ngMock.module('kibana', function ($provide) { - $provide.decorator('docTitle', decorateWithSpy('update')); $provide.decorator('$rootScope', decorateWithSpy('$on')); })); - beforeEach(ngMock.inject(function ($injector, Private) { - if (_.random(0, 1)) { - docTitle = $injector.get('docTitle'); - } else { - docTitle = Private(DocTitleProvider); - } - + beforeEach(ngMock.inject(function ($injector) { $rootScope = $injector.get('$rootScope'); })); diff --git a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/bar/src/index.js b/src/legacy/ui/public/doc_title/doc_title.d.ts similarity index 88% rename from packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/bar/src/index.js rename to src/legacy/ui/public/doc_title/doc_title.d.ts index dd385cf498334..8253a45850e19 100644 --- a/packages/kbn-pm/src/production/integration_tests/__fixtures__/packages/bar/src/index.js +++ b/src/legacy/ui/public/doc_title/doc_title.d.ts @@ -17,8 +17,8 @@ * under the License. */ -import _ from 'lodash'; - -export default function(val) { - return `test second: ${_.upperCase(val)}`; +export interface DocTitle { + change: (title: string) => void; } + +export const docTitle: DocTitle; diff --git a/src/legacy/ui/public/doc_title/doc_title.js b/src/legacy/ui/public/doc_title/doc_title.js index 1a0fde0e2486f..3692fd71f06cc 100644 --- a/src/legacy/ui/public/doc_title/doc_title.js +++ b/src/legacy/ui/public/doc_title/doc_title.js @@ -20,45 +20,50 @@ import _ from 'lodash'; import { uiModules } from '../modules'; -uiModules.get('kibana') - .run(function ($rootScope, docTitle) { - // always bind to the route events - $rootScope.$on('$routeChangeStart', docTitle.reset); - $rootScope.$on('$routeChangeError', docTitle.update); - $rootScope.$on('$routeChangeSuccess', docTitle.update); - }) - .service('docTitle', function () { - const baseTitle = document.title; - const self = this; +let baseTitle = document.title; - let lastChange; +// for karma test +export function setBaseTitle(str) { + baseTitle = str; +} - function render() { - lastChange = lastChange || []; +let lastChange; - const parts = [lastChange[0]]; +function render() { + lastChange = lastChange || []; - if (!lastChange[1]) parts.push(baseTitle); + const parts = [lastChange[0]]; - return _(parts).flattenDeep().compact().join(' - '); - } + if (!lastChange[1]) parts.push(baseTitle); - self.change = function (title, complete) { - lastChange = [title, complete]; - self.update(); - }; + return _(parts).flattenDeep().compact().join(' - '); +} - self.reset = function () { - lastChange = null; - }; +function change(title, complete) { + lastChange = [title, complete]; + update(); +} - self.update = function () { - document.title = render(); - }; - }); +function reset() { + lastChange = null; +} -// return a "private module" so that it can be used both ways -export function DocTitleProvider(docTitle) { - return docTitle; +function update() { + document.title = render(); } +export const docTitle = { + render, + change, + reset, + update, +}; + +uiModules.get('kibana') + .run(function ($rootScope) { + // always bind to the route events + $rootScope.$on('$routeChangeStart', docTitle.reset); + $rootScope.$on('$routeChangeError', docTitle.update); + $rootScope.$on('$routeChangeSuccess', docTitle.update); + }); + diff --git a/src/legacy/ui/public/doc_title/index.js b/src/legacy/ui/public/doc_title/index.js index 06018b0c20dcf..1507797cc749c 100644 --- a/src/legacy/ui/public/doc_title/index.js +++ b/src/legacy/ui/public/doc_title/index.js @@ -19,4 +19,4 @@ import './doc_title'; -export { DocTitleProvider } from './doc_title'; +export { docTitle } from './doc_title'; diff --git a/src/legacy/ui/public/embeddable/embeddable_factory.ts b/src/legacy/ui/public/embeddable/embeddable_factory.ts index e71813b22995f..782b8053498a9 100644 --- a/src/legacy/ui/public/embeddable/embeddable_factory.ts +++ b/src/legacy/ui/public/embeddable/embeddable_factory.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectAttributes } from '../../../server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server'; import { SavedObjectMetaData } from '../saved_objects/components/saved_object_finder'; import { Embeddable } from './embeddable'; import { EmbeddableState } from './types'; diff --git a/src/legacy/ui/public/embeddable/index.ts b/src/legacy/ui/public/embeddable/index.ts index d4ec27eb5a03c..a4d3b08bb1093 100644 --- a/src/legacy/ui/public/embeddable/index.ts +++ b/src/legacy/ui/public/embeddable/index.ts @@ -21,4 +21,4 @@ export { EmbeddableFactory, OnEmbeddableStateChanged } from './embeddable_factor export * from './embeddable'; export * from './context_menu_actions'; export { EmbeddableFactoriesRegistryProvider } from './embeddable_factories_registry'; -export { ContainerState, EmbeddableState, Query, Filters, Filter, TimeRange } from './types'; +export { ContainerState, EmbeddableState } from './types'; diff --git a/src/legacy/ui/public/embeddable/types.ts b/src/legacy/ui/public/embeddable/types.ts index 05247443907d3..0f98b070dda78 100644 --- a/src/legacy/ui/public/embeddable/types.ts +++ b/src/legacy/ui/public/embeddable/types.ts @@ -19,32 +19,8 @@ import { Filter } from '@kbn/es-query'; import { RefreshInterval } from 'ui/timefilter/timefilter'; - -// Should go away soon once everyone imports from kbn/es-query -export { Filter } from '@kbn/es-query'; - -export interface TimeRange { - to: string; - from: string; -} - -export interface FilterMeta { - disabled: boolean; -} - -export type Filters = Filter[]; - -export enum QueryLanguageType { - KUERY = 'kuery', - LUCENE = 'lucene', -} - -// It's a string sometimes in old version formats, before Kuery came along and there -// was the language specifier. -export interface Query { - language: QueryLanguageType; - query: string; -} +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; export interface EmbeddableCustomization { [key: string]: object | string; diff --git a/src/legacy/ui/public/index_patterns/__tests__/_index_patterns.test.js b/src/legacy/ui/public/index_patterns/__tests__/_index_patterns.test.js index f099e0b7be314..6c6273b6daf38 100644 --- a/src/legacy/ui/public/index_patterns/__tests__/_index_patterns.test.js +++ b/src/legacy/ui/public/index_patterns/__tests__/_index_patterns.test.js @@ -80,7 +80,7 @@ const config = { describe('IndexPatterns', () => { - const indexPatterns = new IndexPatterns('', config, savedObjectsClient); + const indexPatterns = new IndexPatterns(config, savedObjectsClient); it('does not cache gets without an id', function () { expect(indexPatterns.get()).not.toBe(indexPatterns.get()); diff --git a/src/legacy/ui/public/index_patterns/__tests__/_index_patterns_api_client.test.js b/src/legacy/ui/public/index_patterns/__tests__/_index_patterns_api_client.test.js new file mode 100644 index 0000000000000..52b8174f0e34f --- /dev/null +++ b/src/legacy/ui/public/index_patterns/__tests__/_index_patterns_api_client.test.js @@ -0,0 +1,47 @@ +/* + * 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 { http } from './_index_patterns_api_client.test.mock'; +import { IndexPatternsApiClient } from '../index_patterns_api_client'; + +jest.mock('../../errors', () => ({ + SavedObjectNotFound: jest.fn(), + DuplicateField: jest.fn(), + IndexPatternMissingIndices: jest.fn(), +})); + +jest.mock('../errors', () => ({ + IndexPatternMissingIndices: jest.fn(), +})); + +describe('IndexPatternsApiClient', () => { + it('uses the right URI to fetch fields for time patterns', async function () { + const fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => ({})); + const indexPatternsApiClient = new IndexPatternsApiClient(); + await indexPatternsApiClient.getFieldsForTimePattern(); + + expect(fetchSpy).toHaveBeenCalledWith('/api/index_patterns/_fields_for_time_pattern', { + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + method: 'GET', + prependBasePath: true, + query: {}, + }); + }); +}); diff --git a/src/legacy/ui/public/management/index_pattern_creation/index_pattern_creation_config_registry.js b/src/legacy/ui/public/index_patterns/__tests__/_index_patterns_api_client.test.mock.js similarity index 75% rename from src/legacy/ui/public/management/index_pattern_creation/index_pattern_creation_config_registry.js rename to src/legacy/ui/public/index_patterns/__tests__/_index_patterns_api_client.test.mock.js index 9fb65a1bcb002..66de7bf3a7f21 100644 --- a/src/legacy/ui/public/management/index_pattern_creation/index_pattern_creation_config_registry.js +++ b/src/legacy/ui/public/index_patterns/__tests__/_index_patterns_api_client.test.mock.js @@ -17,10 +17,11 @@ * under the License. */ -import { uiRegistry } from 'ui/registry/_registry'; -export const IndexPatternCreationConfigRegistry = uiRegistry({ - name: 'indexPatternCreation', - index: ['name'], - order: ['order'], +import { setup } from '../../../../../test_utils/public/http_test_setup'; + +export const { http } = setup(injectedMetadata => { + injectedMetadata.getBasePath.mockReturnValue('/hola/daro/'); }); + +jest.doMock('ui/new_platform', () => ({ npSetup: { core: { http } } })); diff --git a/src/legacy/ui/public/index_patterns/index_patterns.js b/src/legacy/ui/public/index_patterns/index_patterns.js index ab5712c3f7335..9d093e73f2a45 100644 --- a/src/legacy/ui/public/index_patterns/index_patterns.js +++ b/src/legacy/ui/public/index_patterns/index_patterns.js @@ -27,9 +27,9 @@ import { FieldsFetcher } from './fields_fetcher'; import { IndexPatternsApiClient } from './index_patterns_api_client'; export class IndexPatterns { - constructor(basePath, config, savedObjectsClient) { + constructor(config, savedObjectsClient) { const getProvider = indexPatternsGetProvider(savedObjectsClient); - const apiClient = new IndexPatternsApiClient(basePath); + const apiClient = new IndexPatternsApiClient(); this.config = config; this.savedObjectsClient = savedObjectsClient; @@ -84,11 +84,11 @@ import { uiModules } from '../modules'; const module = uiModules.get('kibana/index_patterns'); let _service; module.service('indexPatterns', function (chrome) { - if (!_service) _service = new IndexPatterns(chrome.getBasePath(), chrome.getUiSettingsClient(), chrome.getSavedObjectsClient()); + if (!_service) _service = new IndexPatterns(chrome.getUiSettingsClient(), chrome.getSavedObjectsClient()); return _service; }); export const IndexPatternsProvider = (chrome) => { - if (!_service) _service = new IndexPatterns(chrome.getBasePath(), chrome.getUiSettingsClient(), chrome.getSavedObjectsClient()); + if (!_service) _service = new IndexPatterns(chrome.getUiSettingsClient(), chrome.getSavedObjectsClient()); return _service; }; diff --git a/src/legacy/ui/public/index_patterns/index_patterns_api_client.js b/src/legacy/ui/public/index_patterns/index_patterns_api_client.js index 357c6adccaa4e..96ea1423129f2 100644 --- a/src/legacy/ui/public/index_patterns/index_patterns_api_client.js +++ b/src/legacy/ui/public/index_patterns/index_patterns_api_client.js @@ -45,8 +45,8 @@ function request(method, url, query, body) { } export class IndexPatternsApiClient { - constructor(basePath) { - this.apiBaseUrl = `${basePath}/api/index_patterns/`; + constructor() { + this.apiBaseUrl = `/api/index_patterns/`; } _getUrl(path) { diff --git a/src/legacy/ui/public/management/index_pattern_creation/index.js b/src/legacy/ui/public/management/index_pattern_creation/index.js index 6b1dbcc084e2f..0b677cbfd1f64 100644 --- a/src/legacy/ui/public/management/index_pattern_creation/index.js +++ b/src/legacy/ui/public/management/index_pattern_creation/index.js @@ -20,4 +20,4 @@ import './register'; export { IndexPatternCreationFactory } from './index_pattern_creation'; export { IndexPatternCreationConfig } from './index_pattern_creation_config'; -export { IndexPatternCreationConfigRegistry } from './index_pattern_creation_config_registry'; +export { indexPatternTypes, addIndexPatternType } from './index_pattern_types'; diff --git a/src/legacy/ui/public/management/index_pattern_creation/index_pattern_creation.js b/src/legacy/ui/public/management/index_pattern_creation/index_pattern_creation.js index 4e44173151080..12cecc956ab68 100644 --- a/src/legacy/ui/public/management/index_pattern_creation/index_pattern_creation.js +++ b/src/legacy/ui/public/management/index_pattern_creation/index_pattern_creation.js @@ -17,17 +17,16 @@ * under the License. */ -import { IndexPatternCreationConfigRegistry } from './index_pattern_creation_config_registry'; +import { indexPatternTypes } from './index_pattern_types'; class IndexPatternCreation { - constructor(registry, httpClient, type) { - this._registry = registry; - this._allTypes = this._registry.inOrder.map(Plugin => new Plugin({ httpClient })); + constructor(httpClient, type) { + this._allTypes = indexPatternTypes.map(Plugin => new Plugin({ httpClient })); this._setCurrentType(type); } _setCurrentType = (type) => { - const index = type ? this._registry.inOrder.findIndex(Plugin => Plugin.key === type) : -1; + const index = type ? indexPatternTypes.findIndex(Plugin => Plugin.key === type) : -1; this._currentType = index > -1 && this._allTypes[index] ? this._allTypes[index] : null; } @@ -49,8 +48,7 @@ class IndexPatternCreation { export const IndexPatternCreationFactory = (Private, $http) => { return (type = 'default') => { - const indexPatternCreationRegistry = Private(IndexPatternCreationConfigRegistry); - const indexPatternCreationProvider = new IndexPatternCreation(indexPatternCreationRegistry, $http, type); + const indexPatternCreationProvider = new IndexPatternCreation($http, type); return indexPatternCreationProvider; }; }; diff --git a/src/legacy/ui/public/management/index_pattern_creation/index_pattern_types.js b/src/legacy/ui/public/management/index_pattern_creation/index_pattern_types.js new file mode 100644 index 0000000000000..9e94a52278ab2 --- /dev/null +++ b/src/legacy/ui/public/management/index_pattern_creation/index_pattern_types.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const indexPatternTypes = []; +export const addIndexPatternType = (type) => indexPatternTypes.push(type); diff --git a/src/legacy/ui/public/management/index_pattern_creation/register.js b/src/legacy/ui/public/management/index_pattern_creation/register.js index 764c275fdf3a0..bca4387b496fd 100644 --- a/src/legacy/ui/public/management/index_pattern_creation/register.js +++ b/src/legacy/ui/public/management/index_pattern_creation/register.js @@ -18,6 +18,6 @@ */ import { IndexPatternCreationConfig } from './index_pattern_creation_config'; -import { IndexPatternCreationConfigRegistry } from './index_pattern_creation_config_registry'; +import { addIndexPatternType } from './index_pattern_types'; -IndexPatternCreationConfigRegistry.register(() => IndexPatternCreationConfig); +addIndexPatternType(IndexPatternCreationConfig); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 2880cbd60bcbd..694e92f352015 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -37,10 +37,12 @@ export function __reset__() { npStart.core = (null as unknown) as InternalCoreStart; } -export function __setup__(coreSetup: InternalCoreSetup) { +export function __setup__(coreSetup: InternalCoreSetup, plugins: Record) { npSetup.core = coreSetup; + npSetup.plugins = plugins; } -export function __start__(coreStart: InternalCoreStart) { +export function __start__(coreStart: InternalCoreStart, plugins: Record) { npStart.core = coreStart; + npStart.plugins = plugins; } diff --git a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx index 69aecc8b78625..a5930ee38a662 100644 --- a/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx +++ b/src/legacy/ui/public/saved_objects/components/saved_object_finder.tsx @@ -45,7 +45,7 @@ import { import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; -import { SavedObjectAttributes } from '../../../../server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server'; import { SimpleSavedObject } from '../simple_saved_object'; // TODO the typings for EuiListGroup are incorrect - maxWidth is missing. This can be removed when the types are adjusted diff --git a/src/legacy/ui/public/saved_objects/find_object_by_title.ts b/src/legacy/ui/public/saved_objects/find_object_by_title.ts index 5e83dd32e80eb..e27326249f5c9 100644 --- a/src/legacy/ui/public/saved_objects/find_object_by_title.ts +++ b/src/legacy/ui/public/saved_objects/find_object_by_title.ts @@ -18,7 +18,7 @@ */ import { find } from 'lodash'; -import { SavedObjectAttributes } from '../../../server/saved_objects'; +import { SavedObjectAttributes } from 'src/core/server'; import { SavedObjectsClient } from './saved_objects_client'; import { SimpleSavedObject } from './simple_saved_object'; diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts b/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts index f922d4446c3a3..fed3b8807cddd 100644 --- a/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts +++ b/src/legacy/ui/public/saved_objects/saved_objects_client.test.ts @@ -20,7 +20,7 @@ jest.mock('ui/kfetch', () => ({})); import * as sinon from 'sinon'; -import { FindOptions } from '../../../server/saved_objects/service'; +import { SavedObjectsFindOptions } from 'src/core/server'; import { SavedObjectsClient } from './saved_objects_client'; import { SimpleSavedObject } from './simple_saved_object'; @@ -341,7 +341,7 @@ describe('SavedObjectsClient', () => { }); test('accepts pagination params', () => { - const options: FindOptions = { perPage: 10, page: 6 }; + const options: SavedObjectsFindOptions = { perPage: 10, page: 6 }; savedObjectsClient.find(options); sinon.assert.calledOnce(kfetchStub); diff --git a/src/legacy/ui/public/saved_objects/saved_objects_client.ts b/src/legacy/ui/public/saved_objects/saved_objects_client.ts index 6fee8826d1638..d8fb3af1c66ac 100644 --- a/src/legacy/ui/public/saved_objects/saved_objects_client.ts +++ b/src/legacy/ui/public/saved_objects/saved_objects_client.ts @@ -21,13 +21,13 @@ import { cloneDeep, pick, throttle } from 'lodash'; import { resolve as resolveUrl } from 'url'; import { - MigrationVersion, SavedObject, SavedObjectAttributes, SavedObjectReference, - SavedObjectsClient as SavedObjectsApi, -} from '../../../server/saved_objects'; -import { FindOptions } from '../../../server/saved_objects/service'; + SavedObjectsClientContract as SavedObjectsApi, + SavedObjectsFindOptions, + SavedObjectsMigrationVersion, +} from 'src/core/server'; import { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from '../error_auto_create_index'; import { kfetch, KFetchQuery } from '../kfetch'; import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../utils/case_conversion'; @@ -43,7 +43,7 @@ interface RequestParams { interface CreateOptions { id?: string; overwrite?: boolean; - migrationVersion?: MigrationVersion; + migrationVersion?: SavedObjectsMigrationVersion; references?: SavedObjectReference[]; } @@ -55,7 +55,7 @@ interface BulkCreateOptions( - options: FindOptions = {} + options: SavedObjectsFindOptions = {} ): Promise> => { const path = this.getPath(['_find']); const query = keysToSnakeCaseShallow(options); diff --git a/src/legacy/ui/public/saved_objects/simple_saved_object.ts b/src/legacy/ui/public/saved_objects/simple_saved_object.ts index d742b103afdd0..26488bdeb1ab1 100644 --- a/src/legacy/ui/public/saved_objects/simple_saved_object.ts +++ b/src/legacy/ui/public/saved_objects/simple_saved_object.ts @@ -18,10 +18,7 @@ */ import { get, has, set } from 'lodash'; -import { - SavedObject as SavedObjectType, - SavedObjectAttributes, -} from '../../../server/saved_objects'; +import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/server'; import { SavedObjectsClient } from './saved_objects_client'; /** diff --git a/src/legacy/ui/public/timefilter/get_time.ts b/src/legacy/ui/public/timefilter/get_time.ts index 8fdbd2a133349..eeb09cdfe3743 100644 --- a/src/legacy/ui/public/timefilter/get_time.ts +++ b/src/legacy/ui/public/timefilter/get_time.ts @@ -19,16 +19,12 @@ import dateMath from '@elastic/datemath'; import { Field, IndexPattern } from 'ui/index_patterns'; +import { TimeRange } from './time_history'; interface CalculateBoundsOptions { forceNow?: Date; } -interface TimeRange { - to: string; - from: string; -} - interface RangeFilter { gte?: string | number; lte?: string | number; @@ -68,7 +64,9 @@ export function getTime( if (!bounds) { return; } - const filter: Filter = { range: { [timefield.name]: { format: 'strict_date_optional_time' } } }; + const filter: Filter = { + range: { [timefield.name]: { format: 'strict_date_optional_time' } }, + }; if (bounds.min) { filter.range[timefield.name].gte = bounds.min.toISOString(); diff --git a/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts b/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts index 9b3687b92a541..f2475065a869c 100644 --- a/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts +++ b/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts @@ -17,11 +17,13 @@ * under the License. */ +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; import { SearchSource } from '../../courier'; import { QueryFilter } from '../../filter_manager/query_filter'; import { Adapters } from '../../inspector/types'; import { PersistedState } from '../../persisted_state'; -import { Filters, Query, TimeRange } from '../../visualize'; import { AggConfigs } from '../agg_configs'; import { Vis } from '../vis'; @@ -30,7 +32,7 @@ export interface RequestHandlerParams { aggs: AggConfigs; timeRange?: TimeRange; query?: Query; - filters?: Filters; + filters?: Filter[]; forceFetch: boolean; queryFilter: QueryFilter; uiState?: PersistedState; diff --git a/src/legacy/ui/public/vis/vis_filters/vis_filters.js b/src/legacy/ui/public/vis/vis_filters/vis_filters.js index 72b79474cba18..f013233c345b2 100644 --- a/src/legacy/ui/public/vis/vis_filters/vis_filters.js +++ b/src/legacy/ui/public/vis/vis_filters/vis_filters.js @@ -20,6 +20,8 @@ import _ from 'lodash'; import { pushFilterBarFilters } from '../../filter_manager/push_filters'; import { onBrushEvent } from './brush_event'; +import { uniqFilters } from '../../filter_manager/lib/uniq_filters'; +import { toggleFilterNegated } from '@kbn/es-query'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -93,7 +95,7 @@ const createFiltersFromEvent = (event) => { if (filter) { filter.forEach(f => { if (event.negate) { - f.meta.negate = !f.meta.negate; + f = toggleFilterNegated(f); } filters.push(f); }); @@ -108,11 +110,7 @@ const VisFiltersProvider = (getAppState, $timeout) => { const pushFilters = (filters, simulate) => { const appState = getAppState(); if (filters.length && !simulate) { - const flatFilters = _.flatten(filters); - const deduplicatedFilters = flatFilters.filter((v, i) => { - return i === flatFilters.findIndex(f => _.isEqual(v, f)); - }); - pushFilterBarFilters(appState, deduplicatedFilters); + pushFilterBarFilters(appState, uniqFilters(filters)); // to trigger angular digest cycle, we can get rid of this once we have either new filterManager or actions API $timeout(_.noop, 0); } diff --git a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js index c3329cb0307f2..ce94c3a5f68ab 100644 --- a/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js +++ b/src/legacy/ui/public/vis/vis_types/vislib_vis_legend.js @@ -22,15 +22,14 @@ import { i18n } from '@kbn/i18n'; import html from './vislib_vis_legend.html'; import { Data } from '../../vislib/lib/data'; import { uiModules } from '../../modules'; -import { VisFiltersProvider } from '../vis_filters'; +import { createFiltersFromEvent } from '../vis_filters'; import { htmlIdGenerator, keyCodes } from '@elastic/eui'; import { getTableAggs } from '../../visualize/loader/pipeline_helpers/utilities'; export const CUSTOM_LEGEND_VIS_TYPES = ['heatmap', 'gauge']; uiModules.get('kibana') - .directive('vislibLegend', function (Private, $timeout) { - const visFilters = Private(VisFiltersProvider); + .directive('vislibLegend', function ($timeout) { return { restrict: 'E', @@ -97,7 +96,7 @@ uiModules.get('kibana') if (CUSTOM_LEGEND_VIS_TYPES.includes($scope.vis.vislibVis.visConfigArgs.type)) { return false; } - const filters = visFilters.filter({ aggConfigs: $scope.tableAggs, data: legendData.values }, { simulate: true }); + const filters = createFiltersFromEvent({ aggConfigs: $scope.tableAggs, data: legendData.values }); return filters.length; }; diff --git a/src/legacy/ui/public/vis/vis_update.js b/src/legacy/ui/public/vis/vis_update.js index 0a6c1f5e06627..bfa44ef954af8 100644 --- a/src/legacy/ui/public/vis/vis_update.js +++ b/src/legacy/ui/public/vis/vis_update.js @@ -27,12 +27,14 @@ const updateVisualizationConfig = (stateConfig, config) => { // update value axis options const isUserDefinedYAxis = config.setYExtents; + const defaultYExtents = config.defaultYExtents; const mode = ['stacked', 'overlap'].includes(config.mode) ? 'normal' : config.mode || 'normal'; config.valueAxes[0].scale = { ...config.valueAxes[0].scale, type: config.scale || 'linear', setYExtents: config.setYExtents || false, defaultYExtents: config.defaultYExtents || false, + boundsMargin: defaultYExtents ? config.boundsMargin : 0, min: isUserDefinedYAxis ? config.yAxis.min : undefined, max: isUserDefinedYAxis ? config.yAxis.max : undefined, mode: mode diff --git a/src/legacy/ui/public/vislib/__tests__/visualizations/area_chart.js b/src/legacy/ui/public/vislib/__tests__/visualizations/area_chart.js index 1390eecfa28dd..80d6a0e95f6a7 100644 --- a/src/legacy/ui/public/vislib/__tests__/visualizations/area_chart.js +++ b/src/legacy/ui/public/vislib/__tests__/visualizations/area_chart.js @@ -225,5 +225,35 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) { }); }); }); + [0, 2, 4, 8].forEach(function (boundsMarginValue) { + describe('defaultYExtents is true and boundsMargin is defined', function () { + beforeEach(function () { + vis.visConfigArgs.defaultYExtents = true; + vis.visConfigArgs.boundsMargin = boundsMarginValue; + vis.render(dataType, persistedState); + }); + + it('should return yAxis extents equal to data extents with boundsMargin', function () { + vis.handler.charts.forEach(function (chart) { + const yAxis = chart.handler.valueAxes[0]; + const min = vis.handler.valueAxes[0].axisScale.getYMin(); + const max = vis.handler.valueAxes[0].axisScale.getYMax(); + const domain = yAxis.getScale().domain(); + if (min < 0 && max < 0) { + expect(domain[0]).to.equal(min); + expect(domain[1] - boundsMarginValue).to.equal(max); + } + else if (min > 0 && max > 0) { + expect(domain[0] + boundsMarginValue).to.equal(min); + expect(domain[1]).to.equal(max); + } + else { + expect(domain[0]).to.equal(min); + expect(domain[1]).to.equal(max); + } + }); + }); + }); + }); }); }); diff --git a/src/legacy/ui/public/vislib/__tests__/visualizations/column_chart.js b/src/legacy/ui/public/vislib/__tests__/visualizations/column_chart.js index 18ed4b4e28f21..90e94b612014a 100644 --- a/src/legacy/ui/public/vislib/__tests__/visualizations/column_chart.js +++ b/src/legacy/ui/public/vislib/__tests__/visualizations/column_chart.js @@ -208,6 +208,36 @@ dataTypesArray.forEach(function (dataType) { }); }); }); + [0, 2, 4, 8].forEach(function (boundsMarginValue) { + describe('defaultYExtents is true and boundsMargin is defined', function () { + beforeEach(function () { + vis.visConfigArgs.defaultYExtents = true; + vis.visConfigArgs.boundsMargin = boundsMarginValue; + vis.render(data, persistedState); + }); + + it('should return yAxis extents equal to data extents with boundsMargin', function () { + vis.handler.charts.forEach(function (chart) { + const yAxis = chart.handler.valueAxes[0]; + const min = vis.handler.valueAxes[0].axisScale.getYMin(); + const max = vis.handler.valueAxes[0].axisScale.getYMax(); + const domain = yAxis.getScale().domain(); + if (min < 0 && max < 0) { + expect(domain[0]).to.equal(min); + expect(domain[1] - boundsMarginValue).to.equal(max); + } + else if (min > 0 && max > 0) { + expect(domain[0] + boundsMarginValue).to.equal(min); + expect(domain[1]).to.equal(max); + } + else { + expect(domain[0]).to.equal(min); + expect(domain[1]).to.equal(max); + } + }); + }); + }); + }); }); }); diff --git a/src/legacy/ui/public/vislib/__tests__/visualizations/line_chart.js b/src/legacy/ui/public/vislib/__tests__/visualizations/line_chart.js index 98133413c8d97..74456f7ced57e 100644 --- a/src/legacy/ui/public/vislib/__tests__/visualizations/line_chart.js +++ b/src/legacy/ui/public/vislib/__tests__/visualizations/line_chart.js @@ -185,6 +185,36 @@ describe('Vislib Line Chart', function () { }); }); }); + [0, 2, 4, 8].forEach(function (boundsMarginValue) { + describe('defaultYExtents is true and boundsMargin is defined', function () { + beforeEach(function () { + vis.visConfigArgs.defaultYExtents = true; + vis.visConfigArgs.boundsMargin = boundsMarginValue; + vis.render(data, persistedState); + }); + + it('should return yAxis extents equal to data extents with boundsMargin', function () { + vis.handler.charts.forEach(function (chart) { + const yAxis = chart.handler.valueAxes[0]; + const min = vis.handler.valueAxes[0].axisScale.getYMin(); + const max = vis.handler.valueAxes[0].axisScale.getYMax(); + const domain = yAxis.getScale().domain(); + if (min < 0 && max < 0) { + expect(domain[0]).to.equal(min); + expect(domain[1] - boundsMarginValue).to.equal(max); + } + else if (min > 0 && max > 0) { + expect(domain[0] + boundsMarginValue).to.equal(min); + expect(domain[1]).to.equal(max); + } + else { + expect(domain[0]).to.equal(min); + expect(domain[1]).to.equal(max); + } + }); + }); + }); + }); }); }); }); diff --git a/src/legacy/ui/public/vislib/lib/axis/axis_config.js b/src/legacy/ui/public/vislib/lib/axis/axis_config.js index e1ac97da5e848..c19cb4de09713 100644 --- a/src/legacy/ui/public/vislib/lib/axis/axis_config.js +++ b/src/legacy/ui/public/vislib/lib/axis/axis_config.js @@ -30,8 +30,9 @@ const defaults = { type: 'linear', expandLastBucket: true, inverted: false, - setYExtents: null, defaultYExtents: null, + boundsMargin: 0, + setYExtents: null, min: null, max: null, mode: SCALE_MODES.NORMAL diff --git a/src/legacy/ui/public/vislib/lib/axis/axis_scale.js b/src/legacy/ui/public/vislib/lib/axis/axis_scale.js index a6322aa82cc4f..807e2adbf3324 100644 --- a/src/legacy/ui/public/vislib/lib/axis/axis_scale.js +++ b/src/legacy/ui/public/vislib/lib/axis/axis_scale.js @@ -151,7 +151,20 @@ export class AxisScale { const domain = [min, max]; if (this.axisConfig.isUserDefined()) return this.validateUserExtents(domain); if (this.axisConfig.isLogScale()) return this.logDomain(min, max); - if (this.axisConfig.isYExtents()) return domain; + if (this.axisConfig.isYExtents()) { + const scaleBoundsMargin = this.axisConfig.get('scale.boundsMargin'); + if (scaleBoundsMargin === 0) { + return domain; + } else { + if (max < 0) { + domain[1] = domain[1] + scaleBoundsMargin; + } + if (min > 0) { + domain[0] = domain[0] - scaleBoundsMargin; + } + return domain; + } + } return [Math.min(0, min), Math.max(0, max)]; } diff --git a/src/legacy/ui/public/vislib/lib/types/point_series.js b/src/legacy/ui/public/vislib/lib/types/point_series.js index 341aabb85c6a3..24c90cfc1d900 100644 --- a/src/legacy/ui/public/vislib/lib/types/point_series.js +++ b/src/legacy/ui/public/vislib/lib/types/point_series.js @@ -85,6 +85,7 @@ function create(opts) { return function (cfg, data) { const isUserDefinedYAxis = cfg.setYExtents; + const defaultYExtents = cfg.defaultYExtents; const config = _.cloneDeep(cfg); _.defaultsDeep(config, { chartTitle: {}, @@ -110,6 +111,7 @@ function create(opts) { type: config.scale, setYExtents: config.setYExtents, defaultYExtents: config.defaultYExtents, + boundsMargin: defaultYExtents ? config.boundsMargin : 0, min: isUserDefinedYAxis ? config.yAxis.min : undefined, max: isUserDefinedYAxis ? config.yAxis.max : undefined, mode: mode diff --git a/src/legacy/ui/public/visualize/index.ts b/src/legacy/ui/public/visualize/index.ts index ed41c22aebda1..46a8968358294 100644 --- a/src/legacy/ui/public/visualize/index.ts +++ b/src/legacy/ui/public/visualize/index.ts @@ -18,4 +18,3 @@ */ export * from './loader'; -export { Filters, Query, TimeRange } from './loader/types'; diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts index 11ef49f2a0b7b..431c5ae9be6a1 100644 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts +++ b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts @@ -33,7 +33,7 @@ import { RenderCompleteHelper } from '../../render_complete'; import { AppState } from '../../state_management/app_state'; import { timefilter } from '../../timefilter'; import { RequestHandlerParams, Vis } from '../../vis'; -// @ts-ignore +// @ts-ignore untyped dependency import { VisFiltersProvider } from '../../vis/vis_filters'; import { PipelineDataLoader } from './pipeline_data_loader'; import { visualizationLoader } from './visualization_loader'; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap index 920a29ba2f5b6..0eeb18db70850 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap @@ -36,4 +36,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles timelion function 1`] = `"timelion_vis expression='foo' interval='bar' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles undefined markdown function 1`] = `"markdownvis '' fontSize=12 openLinksInNewTab=true "`; + exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js index d8d43a1049980..435108c8023e7 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js @@ -92,6 +92,12 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); + it('handles undefined markdown function', () => { + const params = { fontSize: 12, openLinksInNewTab: true, foo: 'bar' }; + const actual = buildPipelineVisFunction.markdown({ params }); + expect(actual).toMatchSnapshot(); + }); + describe('handles table function', () => { it('without splits or buckets', () => { const params = { foo: 'bar' }; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index b26be35e6a268..1488fa0658912 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -226,7 +226,10 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { }, markdown: visState => { const { markdown, fontSize, openLinksInNewTab } = visState.params; - const escapedMarkdown = escapeString(markdown); + let escapedMarkdown = ''; + if (typeof markdown === 'string' || markdown instanceof String) { + escapedMarkdown = escapeString(markdown.toString()); + } let expr = `markdownvis '${escapedMarkdown}' `; if (fontSize) { expr += ` fontSize=${fontSize} `; diff --git a/src/legacy/ui/public/visualize/loader/types.ts b/src/legacy/ui/public/visualize/loader/types.ts index 410aab9fae83a..33aea5151762e 100644 --- a/src/legacy/ui/public/visualize/loader/types.ts +++ b/src/legacy/ui/public/visualize/loader/types.ts @@ -17,37 +17,14 @@ * under the License. */ +import { TimeRange } from 'ui/timefilter/time_history'; +import { Filter } from '@kbn/es-query'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { SearchSource } from '../../courier'; import { PersistedState } from '../../persisted_state'; import { AppState } from '../../state_management/app_state'; import { Vis } from '../../vis'; -export interface TimeRange { - from: string; - to: string; -} - -export interface FilterMeta { - disabled: boolean; -} - -export interface Filter { - meta: FilterMeta; - query?: object; -} - -export type Filters = Filter[]; - -export enum QueryLanguageType { - KUERY = 'kuery', - LUCENE = 'lucene', -} - -export interface Query { - language: QueryLanguageType; - query: string; -} - export interface VisSavedObject { vis: Vis; description?: string; @@ -96,7 +73,7 @@ export interface VisualizeLoaderParams { /** * Specifies the filters that should be applied to that visualization. */ - filters?: Filters; + filters?: Filter[]; /** * The query that should apply to that visualization. */ diff --git a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts index 860a3a2689c9c..a8f881c9e05aa 100644 --- a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts +++ b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts @@ -22,11 +22,12 @@ import { get } from 'lodash'; import { toastNotifications } from 'ui/notify'; import { AggConfig } from 'ui/vis'; +import { Filter } from '@kbn/es-query'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { Vis } from '../../../vis'; -import { Filters, Query } from '../types'; interface QueryGeohashBoundsParams { - filters?: Filters; + filters?: Filter[]; query?: Query; } diff --git a/src/legacy/ui/ui_bundles/ui_bundles_controller.js b/src/legacy/ui/ui_bundles/ui_bundles_controller.js index ca77683a6cace..f8cfa4c0bbf5b 100644 --- a/src/legacy/ui/ui_bundles/ui_bundles_controller.js +++ b/src/legacy/ui/ui_bundles/ui_bundles_controller.js @@ -26,8 +26,6 @@ import del from 'del'; import { makeRe } from 'minimatch'; import mkdirp from 'mkdirp'; -import { IS_KIBANA_DISTRIBUTABLE } from '../../utils'; - import { UiBundle } from './ui_bundle'; import { appEntryTemplate } from './app_entry_template'; @@ -170,11 +168,7 @@ export class UiBundlesController { } getCacheDirectory(...subPath) { - return this.resolvePath( - '../../built_assets/.cache/ui_bundles', - !IS_KIBANA_DISTRIBUTABLE ? this.hashBundleEntries() : '', - ...subPath - ); + return this.resolvePath('../.cache', this.hashBundleEntries(), ...subPath); } getDescription() { diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 597fbc422e6bf..128a0bac6e327 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -56,6 +56,7 @@ export function uiRenderMixin(kbnServer, server, config) { server.exposeStaticDir('/node_modules/@elastic/eui/dist/{path*}', fromRoot('node_modules/@elastic/eui/dist')); server.exposeStaticDir('/node_modules/@kbn/ui-framework/dist/{path*}', fromRoot('node_modules/@kbn/ui-framework/dist')); + server.exposeStaticDir('/node_modules/@elastic/charts/dist/{path*}', fromRoot('node_modules/@elastic/charts/dist')); const translationsCache = { translations: null, hash: null }; server.route({ @@ -120,9 +121,11 @@ export function uiRenderMixin(kbnServer, server, config) { [ `${basePath}/node_modules/@elastic/eui/dist/eui_theme_dark.css`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, + `${basePath}/node_modules/@elastic/charts/dist/theme_only_dark.css`, ] : [ `${basePath}/node_modules/@elastic/eui/dist/eui_theme_light.css`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, + `${basePath}/node_modules/@elastic/charts/dist/theme_only_light.css`, ] ), `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, diff --git a/src/legacy/ui/ui_settings/__tests__/lib/create_objects_client_stub.js b/src/legacy/ui/ui_settings/__tests__/lib/create_objects_client_stub.js index 457ddd866c758..9a6d44c313248 100644 --- a/src/legacy/ui/ui_settings/__tests__/lib/create_objects_client_stub.js +++ b/src/legacy/ui/ui_settings/__tests__/lib/create_objects_client_stub.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -import { SavedObjectsClient } from '../../../../server/saved_objects'; +import { SavedObjectsClient } from '../../../../../core/server'; export const savedObjectsClientErrors = SavedObjectsClient.errors; diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 01ef4d1f6eaf3..9fae0f0210eff 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -212,13 +212,15 @@ export default class BaseOptimizer { * of Kibana and just make compressing and extracting it more difficult. */ const maybeAddCacheLoader = (cacheName, loaders) => { + if (IS_KIBANA_DISTRIBUTABLE) { + return loaders; + } + return [ { loader: 'cache-loader', options: { - cacheContext: fromRoot('.'), - cacheDirectory: this.uiBundles.getCacheDirectory(cacheName), - readOnly: process.env.KBN_CACHE_LOADER_WRITABLE ? false : IS_KIBANA_DISTRIBUTABLE + cacheDirectory: this.uiBundles.getCacheDirectory(cacheName) } }, ...loaders diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js index 342ac96425a62..97024368c99d7 100644 --- a/src/optimize/dynamic_dll_plugin/dll_config_model.js +++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js @@ -95,13 +95,19 @@ function generateDLL(config) { // Self calling function with the equivalent logic // from maybeAddCacheLoader one from base optimizer use: ((babelLoaderCacheDirPath, loaders) => { + // Only deactivate cache-loader and thread-loader on + // distributable. It is valid when running from source + // both with dev or prod bundles or even when running + // kibana for dev only. + if (IS_KIBANA_DISTRIBUTABLE) { + return loaders; + } + return [ { loader: 'cache-loader', options: { - cacheContext: fromRoot('.'), - cacheDirectory: babelLoaderCacheDirPath, - readOnly: process.env.KBN_CACHE_LOADER_WRITABLE ? false : IS_KIBANA_DISTRIBUTABLE + cacheDirectory: babelLoaderCacheDirPath } }, ...loaders diff --git a/src/plugins/kibana_utils/README.md b/src/plugins/kibana_utils/README.md index 7e3aea954923c..61ceea2b18385 100644 --- a/src/plugins/kibana_utils/README.md +++ b/src/plugins/kibana_utils/README.md @@ -3,4 +3,3 @@ Utilities for building Kibana plugins. - [Store reactive serializable app state in state containers, `createStore`](./docs/store/README.md). -- Store non-serializable or any other state in registries, `createRegistry` diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts index 40af87551574a..b690c7f9e79e1 100644 --- a/src/plugins/testbed/public/plugin.ts +++ b/src/plugins/testbed/public/plugin.ts @@ -23,6 +23,9 @@ export class TestbedPlugin implements Plugin { const es = getService('es'); diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 92e6021eebf47..cdf2d3bf61f53 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -38,6 +38,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'console']); describe('console app', function describeIndexTests() { + this.tags('smoke'); before(async () => { log.debug('navigateTo console'); await PageObjects.common.navigateToApp('console'); diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 19b43f5e3ffd3..344d773214dc6 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); describe('context link in discover', function contextSize() { + this.tags('smoke'); before(async function () { await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setAbsoluteRange(TEST_DISCOVER_START_TIME, TEST_DISCOVER_END_TIME); diff --git a/test/functional/apps/dashboard/bwc_shared_urls.js b/test/functional/apps/dashboard/bwc_shared_urls.js index 69ded31fcc1d1..8b6bdbf20b5cb 100644 --- a/test/functional/apps/dashboard/bwc_shared_urls.js +++ b/test/functional/apps/dashboard/bwc_shared_urls.js @@ -57,8 +57,39 @@ export default function ({ getService, getPageObjects }) { kibanaBaseUrl = currentUrl.substring(0, currentUrl.indexOf('#')); }); - describe('6.0 urls', () => { + describe('5.6 urls', () => { + it('url with filters and query', async () => { + const url56 = `` + + `_g=(refreshInterval:(display:Off,pause:!f,value:0),` + + `time:(from:'2012-11-17T00:00:00.000Z',mode:absolute,to:'2015-11-17T18:01:36.621Z'))&` + + `_a=(` + + `description:'',` + + `filters:!(('$state':(store:appState),` + + `meta:(alias:!n,disabled:!f,index:'logstash-*',key:bytes,negate:!f,type:phrase,value:'12345'),` + + `query:(match:(bytes:(query:12345,type:phrase))))),` + + `fullScreenMode:!f,` + + `options:(),` + + `panels:!((col:1,id:Visualization-MetricChart,panelIndex:1,row:1,size_x:6,size_y:3,type:visualization),` + + `(col:7,id:Visualization-PieChart,panelIndex:2,row:1,size_x:6,size_y:3,type:visualization)),` + + `query:(query_string:(analyze_wildcard:!t,query:'memory:>220000')),` + + `timeRestore:!f,` + + `title:'New+Dashboard',` + + `uiState:(),` + + `viewMode:edit)`; + const url = `${kibanaBaseUrl}#/dashboard?${url56}`; + log.debug(`Navigating to ${url}`); + await browser.get(url, true); + await PageObjects.header.waitUntilLoadingHasFinished(); + + const query = await queryBar.getQueryString(); + expect(query).to.equal('memory:>220000'); + + await pieChart.expectPieSliceCount(0); + await dashboardExpect.panelCount(2); + }); + }); + describe('6.0 urls', () => { it('loads an unsaved dashboard', async function () { const url = `${kibanaBaseUrl}#/dashboard?${urlQuery}`; log.debug(`Navigating to ${url}`); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index c3f426e7d5687..e586443e4909d 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }) { const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['dashboard', 'header', 'visualize']); - describe('dashboard filtering', async () => { + describe('dashboard filtering', async function () { + this.tags('smoke'); before(async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); }); diff --git a/test/functional/apps/dashboard/dashboard_save.js b/test/functional/apps/dashboard/dashboard_save.js index 01e8e370e046d..b4d2cec5722c2 100644 --- a/test/functional/apps/dashboard/dashboard_save.js +++ b/test/functional/apps/dashboard/dashboard_save.js @@ -23,6 +23,7 @@ export default function ({ getPageObjects }) { const PageObjects = getPageObjects(['dashboard', 'header']); describe('dashboard save', function describeIndexTests() { + this.tags('smoke'); const dashboardName = 'Dashboard Save Test'; const dashboardNameEnterKey = 'Dashboard Save Test with Enter Key'; diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index 1fd8baaf42280..9c0c6c17906a0 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -33,6 +33,8 @@ export default function ({ getService, getPageObjects }) { const dashboardName = 'Dashboard Panel Controls Test'; describe('dashboard panel controls', function viewEditModeTests() { + this.tags('smoke'); + before(async function () { await PageObjects.dashboard.initTests(); await browser.refresh(); diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index 10f91448c3705..eca536a1389d5 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -24,7 +24,8 @@ export default function ({ getService, getPageObjects }) { const pieChart = getService('pieChart'); const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']); - describe('dashboard time zones', () => { + describe('dashboard time zones', function () { + this.tags('smoke'); before(async () => { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaSavedObjects(); diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js new file mode 100644 index 0000000000000..97b5cc6bae58c --- /dev/null +++ b/test/functional/apps/discover/_doc_navigation.js @@ -0,0 +1,61 @@ +/* + * 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 expect from '@kbn/expect'; + +const TEST_DOC_START_TIME = '2015-09-19 06:31:44.000'; +const TEST_DOC_END_TIME = '2015-09-23 18:31:44.000'; +const TEST_COLUMN_NAMES = ['@message']; +const TEST_FILTER_COLUMN_NAMES = [['extension', 'jpg'], ['geo.src', 'IN']]; + +export default function ({ getService, getPageObjects }) { + const docTable = getService('docTable'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const esArchiver = getService('esArchiver'); + + describe('doc link in discover', function contextSize() { + this.tags('smoke'); + before(async function () { + await esArchiver.loadIfNeeded('logstash_functional'); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setAbsoluteRange(TEST_DOC_START_TIME, TEST_DOC_END_TIME); + await Promise.all(TEST_COLUMN_NAMES.map((columnName) => ( + PageObjects.discover.clickFieldListItemAdd(columnName) + ))); + await Promise.all(TEST_FILTER_COLUMN_NAMES.map(async ([columnName, value]) => { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + })); + }); + + it('should open the doc view of the selected document', async function () { + const discoverDocTable = await docTable.getTable(); + const firstRow = (await docTable.getBodyRows(discoverDocTable))[0]; + + // navigate to the doc view + await (await docTable.getRowExpandToggle(firstRow)).click(); + const firstDetailsRow = (await docTable.getDetailsRows(discoverDocTable))[0]; + await (await docTable.getRowActions(firstDetailsRow))[1].click(); + + const hasDocHit = await testSubjects.exists('doc-hit'); + expect(hasDocHit).to.be(true); + }); + }); +} diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index f0bda7202232e..828975445e1ef 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -28,6 +28,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); describe('discover tab', function describeIndexTests() { + this.tags('smoke'); before(async function () { const fromTime = '2015-09-19 06:31:44.000'; const toTime = '2015-09-23 18:31:44.000'; diff --git a/test/functional/apps/discover/index.js b/test/functional/apps/discover/index.js index 16520ea838a28..a0b2c43defa24 100644 --- a/test/functional/apps/discover/index.js +++ b/test/functional/apps/discover/index.js @@ -40,6 +40,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_source_filters')); loadTestFile(require.resolve('./_large_string')); loadTestFile(require.resolve('./_inspector')); + loadTestFile(require.resolve('./_doc_navigation')); loadTestFile(require.resolve('./_date_nanos')); }); } diff --git a/test/functional/apps/getting_started/index.js b/test/functional/apps/getting_started/index.js index c5a900be0d1ba..e0495fb366e6f 100644 --- a/test/functional/apps/getting_started/index.js +++ b/test/functional/apps/getting_started/index.js @@ -21,7 +21,7 @@ export default function ({ getService, loadTestFile }) { const browser = getService('browser'); describe('Getting Started ', function () { - this.tags('ciGroup6'); + this.tags(['ciGroup6', 'smoke']); before(async function () { await browser.setWindowSize(1200, 800); diff --git a/test/functional/apps/home/_home.js b/test/functional/apps/home/_home.js index f4a9b53d89ecb..a889f84338acb 100644 --- a/test/functional/apps/home/_home.js +++ b/test/functional/apps/home/_home.js @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'home']); describe('Kibana takes you home', function describeIndexTests() { + this.tags('smoke'); it('clicking on kibana logo should take you to home page', async ()=> { await PageObjects.common.navigateToApp('settings'); diff --git a/test/functional/apps/home/_sample_data.js b/test/functional/apps/home/_sample_data.js index e77996224094f..de14a0d1aaaf3 100644 --- a/test/functional/apps/home/_sample_data.js +++ b/test/functional/apps/home/_sample_data.js @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); describe('sample data', function describeIndexTests() { + this.tags('smoke'); before(async () => { await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index 3539fd975b746..8a875551945bc 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings', 'common']); describe('"Create Index Pattern" wizard', function () { + this.tags('smoke'); before(async function () { // delete .kibana index and then wait for Kibana to re-create it diff --git a/test/functional/apps/visualize/_chart_types.js b/test/functional/apps/visualize/_chart_types.js index 431d9c1b07f0d..6f868b6df464f 100644 --- a/test/functional/apps/visualize/_chart_types.js +++ b/test/functional/apps/visualize/_chart_types.js @@ -44,9 +44,9 @@ export default function ({ getService, getPageObjects }) { 'Metric', 'Pie', 'Region Map', + 'TSVB', 'Tag Cloud', 'Timelion', - 'Timeseries', 'Vega', 'Vertical Bar', ]; diff --git a/test/functional/apps/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index 386185cd9fcc5..8fdc9a18d9907 100644 --- a/test/functional/apps/visualize/_experimental_vis.js +++ b/test/functional/apps/visualize/_experimental_vis.js @@ -23,7 +23,8 @@ export default ({ getService, getPageObjects }) => { const log = getService('log'); const PageObjects = getPageObjects(['common', 'visualize']); - describe('visualize app', () => { + describe('visualize app', function () { + this.tags('smoke'); describe('experimental visualizations', () => { diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index 55f0d4eaac610..3676ed0fd0b6b 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -26,6 +26,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); describe('gauge chart', function indexPatternCreation() { + this.tags('smoke'); const fromTime = '2015-09-19 06:31:44.000'; const toTime = '2015-09-23 18:31:44.000'; diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index 4395bddd77ab9..90530c9a329df 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); describe('heatmap chart', function indexPatternCreation() { + this.tags('smoke'); const vizName1 = 'Visualization HeatmapChart'; const fromTime = '2015-09-19 06:31:44.000'; const toTime = '2015-09-23 18:31:44.000'; diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index 2793bb640e11d..47ecb5c7fcbe6 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); describe('inspector', function describeIndexTests() { + this.tags('smoke'); before(async function () { const fromTime = '2015-09-19 06:31:44.000'; const toTime = '2015-09-23 18:31:44.000'; diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index c34c5acf5ad48..0ca72d7fa7641 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -30,6 +30,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker']); describe('visual builder', function describeIndexTests() { + this.tags('smoke'); beforeEach(async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisualBuilder(); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx index 9bedefd62cccd..a9ec0095b5ea2 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/app.tsx @@ -19,15 +19,12 @@ import { I18nContext } from 'ui/i18n'; import { EuiTab } from '@elastic/eui'; import React, { Component } from 'react'; -import { - IRegistry, - EmbeddableFactory, -} from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; +import { EmbeddableFactory } from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; import { ContactCardEmbeddableExample } from './hello_world_embeddable_example'; import { HelloWorldContainerExample } from './hello_world_container_example'; export interface AppProps { - embeddableFactories: IRegistry; + embeddableFactories: Map; } export class App extends Component { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx index 2faea80769aa1..c614a5f0420ff 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/app/hello_world_container_example.tsx @@ -23,7 +23,6 @@ import { EmbeddablePanel, Container, embeddableFactories, - IRegistry, EmbeddableFactory, } from 'plugins/embeddable_api'; import { @@ -33,7 +32,7 @@ import { } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/test_samples'; interface Props { - embeddableFactories: IRegistry; + embeddableFactories: Map; } export class HelloWorldContainerExample extends React.Component { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx index 590e3e954827f..7e430f6ea119b 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/shim.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { embeddableFactories, IRegistry, EmbeddableFactory } from 'plugins/embeddable_api'; +import { embeddableFactories, EmbeddableFactory } from 'plugins/embeddable_api'; import 'ui/autoload/all'; import 'uiExports/embeddableActions'; @@ -30,7 +30,7 @@ import template from './index.html'; export interface PluginShim { embeddableAPI: { - embeddableFactories: IRegistry; + embeddableFactories: Map; }; } diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 80acdac7a985b..fb532e9ec71db 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -18,4 +18,4 @@ if [ "$CI_GROUP" == "1" ]; then cd -; yarn run grunt run:pluginFunctionalTestsRelease --from=source; yarn run grunt run:interpreterFunctionalTestsRelease; -fi +fi \ No newline at end of file diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh new file mode 100755 index 0000000000000..bf3fe0691aa11 --- /dev/null +++ b/test/scripts/jenkins_firefox_smoke.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e +trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT + +node scripts/build --debug --oss; +linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-oss-*-linux-x86_64.tar.gz')" +installDir="$PARENT_DIR/install/kibana" +mkdir -p "$installDir" +tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + +export TEST_BROWSER_HEADLESS=1 + +checks-reporter-with-killswitch "Firefox smoke test" \ + node scripts/functional_tests \ + --bail --debug \ + --kibana-install-dir "$installDir" \ + --include-tag "smoke" \ + --config test/functional/config.firefox.js; diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index 9bdcd72862925..3527aa5eedfa9 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -46,4 +46,3 @@ echo "" # --config "test/functional/config.firefox.js" # echo "" # echo "" - diff --git a/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh new file mode 100755 index 0000000000000..783e37fdfb515 --- /dev/null +++ b/test/scripts/jenkins_xpack_firefox_smoke.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -e +trap 'node "$KIBANA_DIR/src/dev/failed_tests/cli"' EXIT + +node scripts/build --debug --no-oss; +linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" +installDir="$PARENT_DIR/install/kibana" +mkdir -p "$installDir" +tar -xzf "$linuxBuild" -C "$installDir" --strip=1 + +export TEST_BROWSER_HEADLESS=1 + +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "X-Pack firefox smoke test" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$installDir" \ + --include-tag "smoke" \ + --config test/functional/config.firefox.js; diff --git a/tsconfig.types.json b/tsconfig.types.json index 634a423b96e30..fd3624dd8e31b 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -9,6 +9,7 @@ }, "include": [ "src/core/server/index.ts", - "src/core/public/index.ts" + "src/core/public/index.ts", + "typings" ] } diff --git a/x-pack/index.js b/x-pack/index.js index 6a81b04131cb3..c749e8afe2715 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -39,6 +39,7 @@ import { translations } from './plugins/translations'; import { upgradeAssistant } from './plugins/upgrade_assistant'; import { uptime } from './plugins/uptime'; import { ossTelemetry } from './plugins/oss_telemetry'; +import { fileUpload } from './plugins/file_upload'; import { telemetry } from './plugins/telemetry'; import { encryptedSavedObjects } from './plugins/encrypted_saved_objects'; import { snapshotRestore } from './plugins/snapshot_restore'; @@ -81,6 +82,7 @@ module.exports = function (kibana) { upgradeAssistant(kibana), uptime(kibana), ossTelemetry(kibana), + fileUpload(kibana), encryptedSavedObjects(kibana), snapshotRestore(kibana), ]; diff --git a/x-pack/package.json b/x-pack/package.json index d4df8849a08d6..39cc7096fd775 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -114,9 +114,9 @@ "copy-webpack-plugin": "^5.0.0", "del": "^4.0.0", "dotenv": "2.0.0", - "enzyme": "^3.9.0", - "enzyme-adapter-react-16": "^1.13.1", - "enzyme-adapter-utils": "^1.10.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.14.0", + "enzyme-adapter-utils": "^1.12.0", "enzyme-to-json": "^3.3.4", "execa": "^1.0.0", "fancy-log": "^1.3.2", @@ -170,10 +170,10 @@ "@babel/runtime": "7.4.5", "@elastic/datemath": "5.0.2", "@elastic/eui": "11.3.2", - "@elastic/javascript-typescript-langserver": "^0.1.28", - "@elastic/lsp-extension": "^0.1.1", + "@elastic/javascript-typescript-langserver": "^0.2.0", + "@elastic/lsp-extension": "^0.1.2", "@elastic/node-crypto": "^1.0.0", - "@elastic/nodegit": "0.25.0-alpha.20", + "@elastic/nodegit": "0.25.0-alpha.21", "@elastic/numeral": "2.3.3", "@elastic/request-crypto": "^1.0.2", "@kbn/babel-preset": "1.0.0", @@ -225,6 +225,7 @@ "file-type": "^10.9.0", "font-awesome": "4.7.0", "formsy-react": "^1.1.5", + "geojson-rewind": "^0.3.1", "get-port": "4.2.0", "getos": "^3.1.0", "git-url-parse": "11.1.2", @@ -251,6 +252,7 @@ "js-yaml": "3.13.1", "json-stable-stringify": "^1.0.1", "jsonwebtoken": "^8.3.0", + "jsts": "^2.0.4", "lodash": "npm:@elastic/lodash@3.10.1-kibana1", "lodash.keyby": "^4.6.0", "lodash.lowercase": "^4.3.0", @@ -339,9 +341,7 @@ "unstated": "^2.1.1", "uuid": "3.3.2", "venn.js": "0.2.20", - "vscode-jsonrpc": "^3.6.2", - "vscode-languageserver": "^4.2.1", - "vscode-languageserver-types": "^3.10.0", + "vscode-languageserver": "^5.2.1", "wellknown": "^0.5.0", "xml2js": "^0.4.19", "xregexp": "3.2.0" diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index a8dbffbdb7f4f..45701f414b010 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -599,18 +599,24 @@ export class WatcherFlyout extends Component< {flyoutBody} - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', - { - defaultMessage: 'Create watch' - } - )} - + + + + {i18n.translate( + 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', + { + defaultMessage: 'Create watch' + } + )} + + + ); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 3da27d1a6f8c6..230fcbd5aacab 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -90,6 +90,7 @@ const getFormatYLong = (transactionType: string | undefined) => (t: number) => { interface Props { distribution?: ITransactionDistributionAPIResponse; urlParams: IUrlParams; + loading: boolean; } export const TransactionDistribution: FunctionComponent = ( @@ -97,7 +98,8 @@ export const TransactionDistribution: FunctionComponent = ( ) => { const { distribution, - urlParams: { transactionId, traceId, transactionType } + urlParams: { transactionId, traceId, transactionType }, + loading } = props; const formatYShort = useCallback(getFormatYShort(transactionType), [ @@ -125,11 +127,14 @@ export const TransactionDistribution: FunctionComponent = ( }) }); }, - [distribution] + [distribution, loading] ); useEffect( () => { + if (loading) { + return; + } const selectedSampleIsAvailable = distribution ? !!distribution.buckets.find( bucket => @@ -145,7 +150,7 @@ export const TransactionDistribution: FunctionComponent = ( redirectToDefaultSample(); } }, - [distribution, transactionId, traceId, redirectToDefaultSample] + [distribution, transactionId, traceId, redirectToDefaultSample, loading] ); if (!distribution || !distribution.totalHits || !traceId || !transactionId) { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx index d0f94139af7a9..c7230e917d0eb 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx @@ -119,5 +119,39 @@ export function StickyTransactionProperties({ } ]; + const { user_agent: userAgent } = transaction; + + if (userAgent) { + const { os, device } = userAgent; + const width = '25%'; + stickyProperties.push({ + label: i18n.translate('xpack.apm.transactionDetails.browserLabel', { + defaultMessage: 'Browser' + }), + val: [userAgent.name, userAgent.version].filter(Boolean).join(' '), + truncated: true, + width + }); + + if (os) { + stickyProperties.push({ + label: i18n.translate('xpack.apm.transactionDetails.osLabel', { + defaultMessage: 'OS' + }), + val: os.full, + truncated: true, + width + }); + } + + stickyProperties.push({ + label: i18n.translate('xpack.apm.transactionDetails.deviceLabel', { + defaultMessage: 'OS' + }), + val: device.name, + width + }); + } + return ; } diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index 93ea387aef25a..b0a0a02100b31 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -16,11 +16,15 @@ import { TransactionDistribution } from './Distribution'; import { Transaction } from './Transaction'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; export function TransactionDetails() { const location = useLocation(); const { urlParams } = useUrlParams(); - const { data: distributionData } = useTransactionDistribution(urlParams); + const { + data: distributionData, + status: distributionStatus + } = useTransactionDistribution(urlParams); const { data: transactionDetailsChartsData } = useTransactionDetailsCharts( urlParams ); @@ -49,6 +53,10 @@ export function TransactionDetails() { diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap b/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap index f2a371d61bf90..860a7a3be7ddf 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap @@ -12,6 +12,7 @@ exports[`StickyProperties should render entire component 1`] = ` wrap={true} > {getPropertyLabel(prop)} {getPropertyValue(prop)} diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts index f100e3de4fdbf..423d4c69953f2 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -22,14 +22,20 @@ const getRelativeImpact = ( ); function getWithRelativeImpact(items: TransactionListAPIResponse) { - const impacts = items.map(({ impact }) => impact); + const impacts = items + .map(({ impact }) => impact) + .filter(impact => impact !== null) as number[]; + const impactMin = Math.min(...impacts); const impactMax = Math.max(...impacts); return items.map(item => { return { ...item, - impactRelative: getRelativeImpact(item.impact, impactMin, impactMax) + impactRelative: + item.impact !== null + ? getRelativeImpact(item.impact, impactMin, impactMax) + : null }; }); } diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts index d5a438aacb6cf..6db6e8848ef07 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes } from 'src/legacy/server/saved_objects/service/saved_objects_client'; +import { SavedObjectAttributes } from 'src/core/server'; import { APM_TELEMETRY_DOC_ID, createApmTelementry, diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts index 7156580698540..007d08594d036 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts @@ -6,7 +6,7 @@ import { Server } from 'hapi'; import { countBy } from 'lodash'; -import { SavedObjectAttributes } from 'src/legacy/server/saved_objects/service/saved_objects_client'; +import { SavedObjectAttributes } from 'src/core/server'; import { isAgentName } from '../../../common/agent_name'; import { getSavedObjectsClient } from '../helpers/saved_objects_client'; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 14849371c4ceb..233bc143a8df8 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BucketAgg, ESFilter } from 'elasticsearch'; +import { ESFilter } from 'elasticsearch'; import { ERROR_GROUP_ID, PROCESSOR_EVENT, @@ -61,13 +61,8 @@ export async function getBuckets({ } }; - interface Aggs { - distribution: { - buckets: Array>; - }; - } + const resp = await client.search(params); - const resp = await client.search(params); const buckets = resp.aggregations.distribution.buckets.map(bucket => ({ key: bucket.key, count: bucket.doc_count diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index a76578f7d07c6..33a93fa986db3 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchParams } from 'elasticsearch'; import { idx } from '@kbn/elastic-idx'; import { ERROR_CULPRIT, @@ -37,7 +36,10 @@ export async function getErrorGroups({ }) { const { start, end, uiFiltersES, client, config } = setup; - const params: SearchParams = { + // sort buckets by last occurrence of error + const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; + + const params = { index: config.get('apm_oss.errorIndices'), body: { size: 0, @@ -56,7 +58,11 @@ export async function getErrorGroups({ terms: { field: ERROR_GROUP_ID, size: 500, - order: { _count: sortDirection } + order: sortByLatestOccurrence + ? { + max_timestamp: sortDirection + } + : { _count: sortDirection } }, aggs: { sample: { @@ -72,24 +78,22 @@ export async function getErrorGroups({ sort: [{ '@timestamp': 'desc' }], size: 1 } - } + }, + ...(sortByLatestOccurrence + ? { + max_timestamp: { + max: { + field: '@timestamp' + } + } + } + : {}) } } } } }; - // sort buckets by last occurrence of error - if (sortField === 'latestOccurrenceAt') { - params.body.aggs.error_groups.terms.order = { - max_timestamp: sortDirection - }; - - params.body.aggs.error_groups.aggs.max_timestamp = { - max: { field: '@timestamp' } - }; - } - interface SampleError { '@timestamp': APMError['@timestamp']; error: { @@ -105,44 +109,27 @@ export async function getErrorGroups({ }; } - interface Bucket { - key: string; - doc_count: number; - sample: { - hits: { - total: number; - max_score: number | null; - hits: Array<{ - _source: SampleError; - }>; - }; - }; - } - - interface Aggs { - error_groups: { - buckets: Bucket[]; - }; - } + const resp = await client.search(params); - const resp = await client.search(params); - const buckets = idx(resp, _ => _.aggregations.error_groups.buckets) || []; + // aggregations can be undefined when no matching indices are found. + // this is an exception rather than the rule so the ES type does not account for this. + const hits = (idx(resp, _ => _.aggregations.error_groups.buckets) || []).map( + bucket => { + const source = bucket.sample.hits.hits[0]._source as SampleError; + const message = + idx(source, _ => _.error.log.message) || + idx(source, _ => _.error.exception[0].message); - const hits = buckets.map(bucket => { - const source = bucket.sample.hits.hits[0]._source; - const message = - idx(source, _ => _.error.log.message) || - idx(source, _ => _.error.exception[0].message); - - return { - message, - occurrenceCount: bucket.doc_count, - culprit: idx(source, _ => _.error.culprit), - groupId: idx(source, _ => _.error.grouping_key), - latestOccurrenceAt: source['@timestamp'], - handled: idx(source, _ => _.error.exception[0].handled) - }; - }); + return { + message, + occurrenceCount: bucket.doc_count, + culprit: idx(source, _ => _.error.culprit), + groupId: idx(source, _ => _.error.grouping_key), + latestOccurrenceAt: source['@timestamp'], + handled: idx(source, _ => _.error.exception[0].handled) + }; + } + ); return hits; } diff --git a/x-pack/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts b/x-pack/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts index a40477deaea43..25b74cafae69e 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchParams } from 'elasticsearch'; import { PROCESSOR_EVENT, TRACE_ID, @@ -17,25 +16,14 @@ export interface ErrorsPerTransaction { [transactionId: string]: number; } -interface TraceErrorsAggBucket { - key: string; - doc_count: number; -} - -interface TraceErrorsAggResponse { - transactions: { - buckets: TraceErrorsAggBucket[]; - }; -} - export async function getTraceErrorsPerTransaction( traceId: string, setup: Setup ): Promise { const { start, end, client, config } = setup; - const params: SearchParams = { - index: [config.get('apm_oss.errorIndices')], + const params = { + index: config.get('apm_oss.errorIndices'), body: { size: 0, query: { @@ -57,7 +45,7 @@ export async function getTraceErrorsPerTransaction( } }; - const resp = await client.search(params); + const resp = await client.search(params); return resp.aggregations.transactions.buckets.reduce( (acc, bucket) => ({ diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index 208e33893e589..420c7087c8032 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -90,10 +90,10 @@ export function getESClient(req: Legacy.Request) { const query = (req.query as unknown) as APMRequestQuery; return { - search: async ( - params: SearchParams, + search: async ( + params: U, apmOptions?: APMOptions - ): Promise> => { + ): Promise> => { const nextParams = await getParamsForSearchRequest( req, params, @@ -112,7 +112,7 @@ export function getESClient(req: Legacy.Request) { } return cluster.callWithRequest(req, 'search', nextParams) as Promise< - AggregationSearchResponse + AggregationSearchResponse >; }, index: (params: IndexDocumentParams) => { diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/fetcher.ts deleted file mode 100644 index dd2826e4ea9c5..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/fetcher.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - SERVICE_AGENT_NAME, - PROCESSOR_EVENT, - SERVICE_NAME, - METRIC_JAVA_HEAP_MEMORY_MAX, - METRIC_JAVA_HEAP_MEMORY_COMMITTED, - METRIC_JAVA_HEAP_MEMORY_USED -} from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; -import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; -import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; -import { rangeFilter } from '../../../../helpers/range_filter'; - -export interface HeapMemoryMetrics extends MetricSeriesKeys { - heapMemoryMax: AggValue; - heapMemoryCommitted: AggValue; - heapMemoryUsed: AggValue; -} - -export async function fetch(setup: Setup, serviceName: string) { - const { start, end, uiFiltersES, client, config } = setup; - - const aggs = { - heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } }, - heapMemoryCommitted: { - avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED } - }, - heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } } - }; - - const params = { - index: config.get('apm_oss.metricsIndices'), - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { term: { [SERVICE_AGENT_NAME]: 'java' } }, - { - range: rangeFilter(start, end) - }, - ...uiFiltersES - ] - } - }, - aggs: { - timeseriesData: { - date_histogram: getMetricsDateHistogramParams(start, end), - aggs - }, - ...aggs - } - } - }; - - return client.search>(params); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts index 50c3d05f76105..48c7ee29a16a3 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -6,49 +6,62 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import { + METRIC_JAVA_HEAP_MEMORY_MAX, + METRIC_JAVA_HEAP_MEMORY_COMMITTED, + METRIC_JAVA_HEAP_MEMORY_USED, + SERVICE_AGENT_NAME +} from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; -import { fetch, HeapMemoryMetrics } from './fetcher'; +import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; import { ChartBase } from '../../../types'; -import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; -// TODO: i18n for titles +const series = { + heapMemoryUsed: { + title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesUsed', { + defaultMessage: 'Avg. used' + }), + color: theme.euiColorVis0 + }, + heapMemoryCommitted: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.heapMemorySeriesCommitted', + { + defaultMessage: 'Avg. committed' + } + ), + color: theme.euiColorVis1 + }, + heapMemoryMax: { + title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesMax', { + defaultMessage: 'Avg. limit' + }), + color: theme.euiColorVis2 + } +}; -const chartBase: ChartBase = { +const chartBase: ChartBase = { title: i18n.translate('xpack.apm.agentMetrics.java.heapMemoryChartTitle', { defaultMessage: 'Heap Memory' }), key: 'heap_memory_area_chart', type: 'area', yUnit: 'bytes', - series: { - heapMemoryUsed: { - title: i18n.translate( - 'xpack.apm.agentMetrics.java.heapMemorySeriesUsed', - { - defaultMessage: 'Avg. used' - } - ), - color: theme.euiColorVis0 - }, - heapMemoryCommitted: { - title: i18n.translate( - 'xpack.apm.agentMetrics.java.heapMemorySeriesCommitted', - { - defaultMessage: 'Avg. committed' - } - ), - color: theme.euiColorVis1 - }, - heapMemoryMax: { - title: i18n.translate('xpack.apm.agentMetrics.java.heapMemorySeriesMax', { - defaultMessage: 'Avg. limit' - }), - color: theme.euiColorVis2 - } - } + series }; export async function getHeapMemoryChart(setup: Setup, serviceName: string) { - const result = await fetch(setup, serviceName); - return transformDataToMetricsChart(result, chartBase); + return fetchAndTransformMetrics({ + setup, + serviceName, + chartBase, + aggs: { + heapMemoryMax: { avg: { field: METRIC_JAVA_HEAP_MEMORY_MAX } }, + heapMemoryCommitted: { + avg: { field: METRIC_JAVA_HEAP_MEMORY_COMMITTED } + }, + heapMemoryUsed: { avg: { field: METRIC_JAVA_HEAP_MEMORY_USED } } + }, + additionalFilters: [{ term: { [SERVICE_AGENT_NAME]: 'java' } }] + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/fetcher.ts deleted file mode 100644 index 84ee3a3864734..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/fetcher.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - SERVICE_AGENT_NAME, - PROCESSOR_EVENT, - SERVICE_NAME, - METRIC_JAVA_NON_HEAP_MEMORY_MAX, - METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED, - METRIC_JAVA_NON_HEAP_MEMORY_USED -} from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; -import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; -import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; -import { rangeFilter } from '../../../../helpers/range_filter'; - -export interface NonHeapMemoryMetrics extends MetricSeriesKeys { - nonHeapMemoryCommitted: AggValue; - nonHeapMemoryUsed: AggValue; -} - -export async function fetch(setup: Setup, serviceName: string) { - const { start, end, uiFiltersES, client, config } = setup; - - const aggs = { - nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } }, - nonHeapMemoryCommitted: { - avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED } - }, - nonHeapMemoryUsed: { - avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED } - } - }; - - const params = { - index: config.get('apm_oss.metricsIndices'), - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { term: { [SERVICE_AGENT_NAME]: 'java' } }, - { - range: rangeFilter(start, end) - }, - ...uiFiltersES - ] - } - }, - aggs: { - timeseriesData: { - date_histogram: getMetricsDateHistogramParams(start, end), - aggs - }, - ...aggs - } - } - }; - - return client.search>(params); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts index 2490449c3cc7c..446d9258b4310 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -6,41 +6,61 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import { + METRIC_JAVA_NON_HEAP_MEMORY_MAX, + METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED, + METRIC_JAVA_NON_HEAP_MEMORY_USED, + SERVICE_AGENT_NAME +} from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; -import { fetch, NonHeapMemoryMetrics } from './fetcher'; import { ChartBase } from '../../../types'; -import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; +import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; -const chartBase: ChartBase = { +const series = { + nonHeapMemoryUsed: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.nonHeapMemorySeriesUsed', + { + defaultMessage: 'Avg. used' + } + ), + color: theme.euiColorVis0 + }, + nonHeapMemoryCommitted: { + title: i18n.translate( + 'xpack.apm.agentMetrics.java.nonHeapMemorySeriesCommitted', + { + defaultMessage: 'Avg. committed' + } + ), + color: theme.euiColorVis1 + } +}; + +const chartBase: ChartBase = { title: i18n.translate('xpack.apm.agentMetrics.java.nonHeapMemoryChartTitle', { defaultMessage: 'Non-Heap Memory' }), key: 'non_heap_memory_area_chart', type: 'area', yUnit: 'bytes', - series: { - nonHeapMemoryUsed: { - title: i18n.translate( - 'xpack.apm.agentMetrics.java.nonHeapMemorySeriesUsed', - { - defaultMessage: 'Avg. used' - } - ), - color: theme.euiColorVis0 - }, - nonHeapMemoryCommitted: { - title: i18n.translate( - 'xpack.apm.agentMetrics.java.nonHeapMemorySeriesCommitted', - { - defaultMessage: 'Avg. committed' - } - ), - color: theme.euiColorVis1 - } - } + series }; export async function getNonHeapMemoryChart(setup: Setup, serviceName: string) { - const result = await fetch(setup, serviceName); - return transformDataToMetricsChart(result, chartBase); + return fetchAndTransformMetrics({ + setup, + serviceName, + chartBase, + aggs: { + nonHeapMemoryMax: { avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_MAX } }, + nonHeapMemoryCommitted: { + avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED } + }, + nonHeapMemoryUsed: { + avg: { field: METRIC_JAVA_NON_HEAP_MEMORY_USED } + } + }, + additionalFilters: [{ term: { [SERVICE_AGENT_NAME]: 'java' } }] + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/fetcher.ts deleted file mode 100644 index a74208cc8218c..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/fetcher.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { - SERVICE_AGENT_NAME, - PROCESSOR_EVENT, - SERVICE_NAME, - METRIC_JAVA_THREAD_COUNT -} from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; -import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; -import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; -import { rangeFilter } from '../../../../helpers/range_filter'; - -export interface ThreadCountMetrics extends MetricSeriesKeys { - threadCount: AggValue; -} - -export async function fetch(setup: Setup, serviceName: string) { - const { start, end, uiFiltersES, client, config } = setup; - - const aggs = { - threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } }, - threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } } - }; - - const params = { - index: config.get('apm_oss.metricsIndices'), - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { term: { [SERVICE_AGENT_NAME]: 'java' } }, - { range: rangeFilter(start, end) }, - ...uiFiltersES - ] - } - }, - aggs: { - timeseriesData: { - date_histogram: getMetricsDateHistogramParams(start, end), - aggs - }, - ...aggs - } - } - }; - - return client.search>(params); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts index db6158ff99487..5f53cde44d622 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -6,35 +6,48 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import { + METRIC_JAVA_THREAD_COUNT, + SERVICE_AGENT_NAME +} from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; -import { fetch, ThreadCountMetrics } from './fetcher'; import { ChartBase } from '../../../types'; -import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; +import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; -const chartBase: ChartBase = { +const series = { + threadCount: { + title: i18n.translate('xpack.apm.agentMetrics.java.threadCount', { + defaultMessage: 'Avg. count' + }), + color: theme.euiColorVis0 + }, + threadCountMax: { + title: i18n.translate('xpack.apm.agentMetrics.java.threadCountMax', { + defaultMessage: 'Max count' + }), + color: theme.euiColorVis1 + } +}; + +const chartBase: ChartBase = { title: i18n.translate('xpack.apm.agentMetrics.java.threadCountChartTitle', { defaultMessage: 'Thread Count' }), key: 'thread_count_line_chart', type: 'linemark', yUnit: 'number', - series: { - threadCount: { - title: i18n.translate('xpack.apm.agentMetrics.java.threadCount', { - defaultMessage: 'Avg. count' - }), - color: theme.euiColorVis0 - }, - threadCountMax: { - title: i18n.translate('xpack.apm.agentMetrics.java.threadCountMax', { - defaultMessage: 'Max count' - }), - color: theme.euiColorVis1 - } - } + series }; export async function getThreadCountChart(setup: Setup, serviceName: string) { - const result = await fetch(setup, serviceName); - return transformDataToMetricsChart(result, chartBase); + return fetchAndTransformMetrics({ + setup, + serviceName, + chartBase, + aggs: { + threadCount: { avg: { field: METRIC_JAVA_THREAD_COUNT } }, + threadCountMax: { max: { field: METRIC_JAVA_THREAD_COUNT } } + }, + additionalFilters: [{ term: { [SERVICE_AGENT_NAME]: 'java' } }] + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/fetcher.ts deleted file mode 100644 index 0da5859666a51..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/fetcher.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - METRIC_PROCESS_CPU_PERCENT, - METRIC_SYSTEM_CPU_PERCENT, - PROCESSOR_EVENT, - SERVICE_NAME -} from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; -import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; -import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; -import { rangeFilter } from '../../../../helpers/range_filter'; - -export interface CPUMetrics extends MetricSeriesKeys { - systemCPUAverage: AggValue; - systemCPUMax: AggValue; - processCPUAverage: AggValue; - processCPUMax: AggValue; -} - -export async function fetch(setup: Setup, serviceName: string) { - const { start, end, uiFiltersES, client, config } = setup; - - const aggs = { - systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, - systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } }, - processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } }, - processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } } - }; - - const params = { - index: config.get('apm_oss.metricsIndices'), - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { - range: rangeFilter(start, end) - }, - ...uiFiltersES - ] - } - }, - aggs: { - timeseriesData: { - date_histogram: getMetricsDateHistogramParams(start, end), - aggs - }, - ...aggs - } - } - }; - - return client.search>(params); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts index f7fe92d578e93..67f69456e9e1b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -6,47 +6,63 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; +import { + METRIC_SYSTEM_CPU_PERCENT, + METRIC_PROCESS_CPU_PERCENT +} from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; -import { fetch, CPUMetrics } from './fetcher'; import { ChartBase } from '../../../types'; -import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; +import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; -const chartBase: ChartBase = { +const series = { + systemCPUMax: { + title: i18n.translate('xpack.apm.chart.cpuSeries.systemMaxLabel', { + defaultMessage: 'System max' + }), + color: theme.euiColorVis1 + }, + systemCPUAverage: { + title: i18n.translate('xpack.apm.chart.cpuSeries.systemAverageLabel', { + defaultMessage: 'System average' + }), + color: theme.euiColorVis0 + }, + processCPUMax: { + title: i18n.translate('xpack.apm.chart.cpuSeries.processMaxLabel', { + defaultMessage: 'Process max' + }), + color: theme.euiColorVis7 + }, + processCPUAverage: { + title: i18n.translate('xpack.apm.chart.cpuSeries.processAverageLabel', { + defaultMessage: 'Process average' + }), + color: theme.euiColorVis5 + } +}; + +const chartBase: ChartBase = { title: i18n.translate('xpack.apm.serviceDetails.metrics.cpuUsageChartTitle', { defaultMessage: 'CPU usage' }), key: 'cpu_usage_chart', type: 'linemark', yUnit: 'percent', - series: { - systemCPUMax: { - title: i18n.translate('xpack.apm.chart.cpuSeries.systemMaxLabel', { - defaultMessage: 'System max' - }), - color: theme.euiColorVis1 - }, - systemCPUAverage: { - title: i18n.translate('xpack.apm.chart.cpuSeries.systemAverageLabel', { - defaultMessage: 'System average' - }), - color: theme.euiColorVis0 - }, - processCPUMax: { - title: i18n.translate('xpack.apm.chart.cpuSeries.processMaxLabel', { - defaultMessage: 'Process max' - }), - color: theme.euiColorVis7 - }, - processCPUAverage: { - title: i18n.translate('xpack.apm.chart.cpuSeries.processAverageLabel', { - defaultMessage: 'Process average' - }), - color: theme.euiColorVis5 - } - } + series }; export async function getCPUChartData(setup: Setup, serviceName: string) { - const result = await fetch(setup, serviceName); - return transformDataToMetricsChart(result, chartBase); + const metricsChart = await fetchAndTransformMetrics({ + setup, + serviceName, + chartBase, + aggs: { + systemCPUAverage: { avg: { field: METRIC_SYSTEM_CPU_PERCENT } }, + systemCPUMax: { max: { field: METRIC_SYSTEM_CPU_PERCENT } }, + processCPUAverage: { avg: { field: METRIC_PROCESS_CPU_PERCENT } }, + processCPUMax: { max: { field: METRIC_PROCESS_CPU_PERCENT } } + } + }); + + return metricsChart; } diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/fetcher.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/fetcher.ts deleted file mode 100644 index 96b3160600111..0000000000000 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/fetcher.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - PROCESSOR_EVENT, - SERVICE_NAME, - METRIC_SYSTEM_FREE_MEMORY, - METRIC_SYSTEM_TOTAL_MEMORY -} from '../../../../../../common/elasticsearch_fieldnames'; -import { Setup } from '../../../../helpers/setup_request'; -import { MetricsAggs, MetricSeriesKeys, AggValue } from '../../../types'; -import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; -import { rangeFilter } from '../../../../helpers/range_filter'; - -export interface MemoryMetrics extends MetricSeriesKeys { - memoryUsedAvg: AggValue; - memoryUsedMax: AggValue; -} - -const percentUsedScript = { - lang: 'expression', - source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']` -}; - -export async function fetch(setup: Setup, serviceName: string) { - const { start, end, uiFiltersES, client, config } = setup; - - const aggs = { - memoryUsedAvg: { avg: { script: percentUsedScript } }, - memoryUsedMax: { max: { script: percentUsedScript } } - }; - - const params = { - index: config.get('apm_oss.metricsIndices'), - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'metric' } }, - { - range: rangeFilter(start, end) - }, - { - exists: { - field: METRIC_SYSTEM_FREE_MEMORY - } - }, - { - exists: { - field: METRIC_SYSTEM_TOTAL_MEMORY - } - }, - ...uiFiltersES - ] - } - }, - aggs: { - timeseriesData: { - date_histogram: getMetricsDateHistogramParams(start, end), - aggs - }, - ...aggs - } - } - }; - - return client.search>(params); -} diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index fe9637ab34e69..e372a62a7ce05 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -5,12 +5,28 @@ */ import { i18n } from '@kbn/i18n'; +import { + METRIC_SYSTEM_FREE_MEMORY, + METRIC_SYSTEM_TOTAL_MEMORY +} from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; -import { fetch, MemoryMetrics } from './fetcher'; import { ChartBase } from '../../../types'; -import { transformDataToMetricsChart } from '../../../transform_metrics_chart'; +import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; -const chartBase: ChartBase = { +const series = { + memoryUsedMax: { + title: i18n.translate('xpack.apm.chart.memorySeries.systemMaxLabel', { + defaultMessage: 'Max' + }) + }, + memoryUsedAvg: { + title: i18n.translate('xpack.apm.chart.memorySeries.systemAverageLabel', { + defaultMessage: 'Average' + }) + } +}; + +const chartBase: ChartBase = { title: i18n.translate( 'xpack.apm.serviceDetails.metrics.memoryUsageChartTitle', { @@ -20,21 +36,34 @@ const chartBase: ChartBase = { key: 'memory_usage_chart', type: 'linemark', yUnit: 'percent', - series: { - memoryUsedMax: { - title: i18n.translate('xpack.apm.chart.memorySeries.systemMaxLabel', { - defaultMessage: 'Max' - }) - }, - memoryUsedAvg: { - title: i18n.translate('xpack.apm.chart.memorySeries.systemAverageLabel', { - defaultMessage: 'Average' - }) - } - } + series +}; + +const percentUsedScript = { + lang: 'expression', + source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']` }; export async function getMemoryChartData(setup: Setup, serviceName: string) { - const result = await fetch(setup, serviceName); - return transformDataToMetricsChart(result, chartBase); + return fetchAndTransformMetrics({ + setup, + serviceName, + chartBase, + aggs: { + memoryUsedAvg: { avg: { script: percentUsedScript } }, + memoryUsedMax: { max: { script: percentUsedScript } } + }, + additionalFilters: [ + { + exists: { + field: METRIC_SYSTEM_FREE_MEMORY + } + }, + { + exists: { + field: METRIC_SYSTEM_TOTAL_MEMORY + } + } + ] + }); } diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts new file mode 100644 index 0000000000000..eefa1e0ef201a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PROCESSOR_EVENT, + SERVICE_NAME +} from '../../../common/elasticsearch_fieldnames'; +import { Setup } from '../helpers/setup_request'; +import { getMetricsDateHistogramParams } from '../helpers/metrics'; +import { rangeFilter } from '../helpers/range_filter'; +import { ChartBase } from './types'; +import { transformDataToMetricsChart } from './transform_metrics_chart'; + +interface Aggs { + [key: string]: { + min?: any; + max?: any; + sum?: any; + avg?: any; + }; +} + +interface Filter { + exists?: { + field: string; + }; + term?: { + [key: string]: string; + }; +} + +export async function fetchAndTransformMetrics({ + setup, + serviceName, + chartBase, + aggs, + additionalFilters = [] +}: { + setup: Setup; + serviceName: string; + chartBase: ChartBase; + aggs: T; + additionalFilters?: Filter[]; +}) { + const { start, end, uiFiltersES, client, config } = setup; + + const params = { + index: config.get('apm_oss.metricsIndices'), + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [PROCESSOR_EVENT]: 'metric' } }, + { + range: rangeFilter(start, end) + }, + ...additionalFilters, + ...uiFiltersES + ] + } + }, + aggs: { + timeseriesData: { + date_histogram: getMetricsDateHistogramParams(start, end), + aggs + }, + ...aggs + } + } + }; + + const response = await client.search(params); + + return transformDataToMetricsChart(response, chartBase); +} diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts index e6fff34b37bc4..e077105e9b2e5 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts @@ -3,22 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AggregationSearchResponse } from 'elasticsearch'; -import { MetricsAggs, MetricSeriesKeys, AggValue } from './types'; import { transformDataToMetricsChart } from './transform_metrics_chart'; import { ChartType, YUnit } from '../../../typings/timeseries'; test('transformDataToMetricsChart should transform an ES result into a chart object', () => { - interface TestKeys extends MetricSeriesKeys { - a: AggValue; - b: AggValue; - c: AggValue; - } - - type R = AggregationSearchResponse>; - const response = { - hits: { total: 5000 } as R['hits'], + hits: { total: 5000 }, aggregations: { a: { value: 1000 }, b: { value: 1000 }, @@ -29,24 +19,27 @@ test('transformDataToMetricsChart should transform an ES result into a chart obj a: { value: 10 }, b: { value: 10 }, c: { value: 10 }, - key: 1 - } as R['aggregations']['timeseriesData']['buckets'][0], + key: 1, + doc_count: 0 + }, { a: { value: 20 }, b: { value: 20 }, c: { value: 20 }, - key: 2 - } as R['aggregations']['timeseriesData']['buckets'][0], + key: 2, + doc_count: 0 + }, { a: { value: 30 }, b: { value: 30 }, c: { value: 30 }, - key: 3 - } as R['aggregations']['timeseriesData']['buckets'][0] + key: 3, + doc_count: 0 + } ] } - } as R['aggregations'] - } as R; + } + } as any; const chartBase = { title: 'Test Chart Title', diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 9936b6883a1c7..1acac008c8bf8 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AggregationSearchResponse } from 'elasticsearch'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { ChartBase, MetricsAggs, MetricSeriesKeys } from './types'; +import { AggregationSearchResponse, AggregatedValue } from 'elasticsearch'; +import { ChartBase } from './types'; const colors = [ theme.euiColorVis0, @@ -20,9 +20,33 @@ const colors = [ export type GenericMetricsChart = ReturnType< typeof transformDataToMetricsChart >; -export function transformDataToMetricsChart( - result: AggregationSearchResponse>, - chartBase: ChartBase + +interface AggregatedParams { + body: { + aggs: { + timeseriesData: { + date_histogram: any; + aggs: { + min?: any; + max?: any; + sum?: any; + avg?: any; + }; + }; + } & { + [key: string]: { + min?: any; + max?: any; + sum?: any; + avg?: any; + }; + }; + }; +} + +export function transformDataToMetricsChart( + result: AggregationSearchResponse, + chartBase: ChartBase ) { const { aggregations, hits } = result; const { timeseriesData } = aggregations; @@ -32,20 +56,24 @@ export function transformDataToMetricsChart( key: chartBase.key, yUnit: chartBase.yUnit, totalHits: hits.total, - series: Object.keys(chartBase.series).map((seriesKey, i) => ({ - title: chartBase.series[seriesKey].title, - key: seriesKey, - type: chartBase.type, - color: chartBase.series[seriesKey].color || colors[i], - overallValue: aggregations[seriesKey].value, - data: timeseriesData.buckets.map(bucket => { - const { value } = bucket[seriesKey]; - const y = value === null || isNaN(value) ? null : value; - return { - x: bucket.key, - y - }; - }) - })) + series: Object.keys(chartBase.series).map((seriesKey, i) => { + const agg = aggregations[seriesKey]; + + return { + title: chartBase.series[seriesKey].title, + key: seriesKey, + type: chartBase.type, + color: chartBase.series[seriesKey].color || colors[i], + overallValue: agg.value, + data: timeseriesData.buckets.map(bucket => { + const { value } = bucket[seriesKey] as AggregatedValue; + const y = value === null || isNaN(value) ? null : value; + return { + x: bucket.key, + y + }; + }) + }; + }) }; } diff --git a/x-pack/plugins/apm/server/lib/metrics/types.ts b/x-pack/plugins/apm/server/lib/metrics/types.ts index f234b44733444..622b37162d9dc 100644 --- a/x-pack/plugins/apm/server/lib/metrics/types.ts +++ b/x-pack/plugins/apm/server/lib/metrics/types.ts @@ -3,38 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { ChartType, YUnit } from '../../../typings/timeseries'; -export interface AggValue { - value: number | null; -} - -export interface MetricSeriesKeys { - [key: string]: AggValue; -} - -export interface ChartBase { +export interface ChartBase { title: string; key: string; type: ChartType; yUnit: YUnit; series: { - [key in keyof T]: { + [key: string]: { title: string; color?: string; - } + }; }; } - -export type MetricsAggs = { - timeseriesData: { - buckets: Array< - { - key_as_string: string; // timestamp as string - key: number; // timestamp as epoch milliseconds - doc_count: number; - } & T - >; - }; -} & T; diff --git a/x-pack/plugins/apm/server/lib/services/get_service.ts b/x-pack/plugins/apm/server/lib/services/get_service.ts index 7cbb1825ef7eb..d39397fedd154 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { BucketAgg } from 'elasticsearch'; import { idx } from '@kbn/elastic-idx'; import { PROCESSOR_EVENT, @@ -48,19 +46,11 @@ export async function getService(serviceName: string, setup: Setup) { } }; - interface Aggs { - types: { - buckets: BucketAgg[]; - }; - agents: { - buckets: BucketAgg[]; - }; - } - - const { aggregations } = await client.search(params); + const { aggregations } = await client.search(params); const buckets = idx(aggregations, _ => _.types.buckets) || []; const types = buckets.map(bucket => bucket.key); - const agentName = idx(aggregations, _ => _.agents.buckets[0].key); + const agentName = idx(aggregations, _ => _.agents.buckets[0].key) || ''; + return { serviceName, types, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 35747a88bb806..75410b70e0139 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BucketAgg } from 'elasticsearch'; import { idx } from '@kbn/elastic-idx'; import { PROCESSOR_EVENT, @@ -65,33 +64,14 @@ export async function getServicesItems(setup: Setup) { } }; - interface ServiceBucket extends BucketAgg { - avg: { - value: number; - }; - agents: { - buckets: BucketAgg[]; - }; - events: { - buckets: BucketAgg[]; - }; - environments: { - buckets: BucketAgg[]; - }; - } - - interface Aggs extends BucketAgg { - services: { - buckets: ServiceBucket[]; - }; - } - - const resp = await client.search(params); + const resp = await client.search(params); const aggs = resp.aggregations; + const serviceBuckets = idx(aggs, _ => _.services.buckets) || []; const items = serviceBuckets.map(bucket => { const eventTypes = bucket.events.buckets; + const transactions = eventTypes.find(e => e.key === 'transaction'); const totalTransactions = idx(transactions, _ => _.doc_count) || 0; diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index e5b5249c3ead3..11599d09c1d65 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -37,5 +37,6 @@ export async function getTraceItems(traceId: string, setup: Setup) { }; const resp = await client.search(params); + return resp.hits.hits.map(hit => hit._source); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 13c3c3bfb2539..b909d5ff62a74 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -4,42 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchParams } from 'elasticsearch'; import { TRANSACTION_DURATION, TRANSACTION_NAME } from '../../../common/elasticsearch_fieldnames'; import { PromiseReturnType, StringMap } from '../../../typings/common'; -import { Transaction } from '../../../typings/es_schemas/ui/Transaction'; import { Setup } from '../helpers/setup_request'; -interface Bucket { - key: string; - doc_count: number; - avg: { value: number }; - p95: { values: { '95.0': number } }; - sum: { value: number }; - sample: { - hits: { - total: number; - max_score: number | null; - hits: Array<{ - _source: Transaction; - }>; - }; - }; -} - -interface Aggs { - transactions: { - buckets: Bucket[]; - }; -} - export type ESResponse = PromiseReturnType; export function transactionGroupsFetcher(setup: Setup, bodyQuery: StringMap) { const { client, config } = setup; - const params: SearchParams = { + const params = { index: config.get('apm_oss.transactionIndices'), body: { size: 0, @@ -72,5 +47,5 @@ export function transactionGroupsFetcher(setup: Setup, bodyQuery: StringMap) { } }; - return client.search(params); + return client.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts index dbef10672c988..62d212d07d2a7 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts @@ -6,16 +6,23 @@ import moment from 'moment'; import { idx } from '@kbn/elastic-idx'; +import { Transaction } from '../../../typings/es_schemas/ui/Transaction'; import { ESResponse } from './fetcher'; function calculateRelativeImpacts(transactionGroups: ITransactionGroup[]) { - const values = transactionGroups.map(({ impact }) => impact); + const values = transactionGroups + .map(({ impact }) => impact) + .filter(value => value !== null) as number[]; + const max = Math.max(...values); const min = Math.min(...values); return transactionGroups.map(bucket => ({ ...bucket, - impact: ((bucket.impact - min) / (max - min)) * 100 || 0 + impact: + bucket.impact !== null + ? ((bucket.impact - min) / (max - min)) * 100 || 0 + : 0 })); } @@ -27,7 +34,7 @@ function getTransactionGroup( const averageResponseTime = bucket.avg.value; const transactionsPerMinute = bucket.doc_count / minutes; const impact = bucket.sum.value; - const sample = bucket.sample.hits.hits[0]._source; + const sample = bucket.sample.hits.hits[0]._source as Transaction; return { name: bucket.key, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts index 6fcf50b148318..b8af24b840d99 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts @@ -8,28 +8,11 @@ import { getMlIndex } from '../../../../../common/ml_job_constants'; import { PromiseReturnType } from '../../../../../typings/common'; import { Setup } from '../../../helpers/setup_request'; -export interface ESBucket { - key_as_string: string; // timestamp as string - key: number; // timestamp - doc_count: number; - anomaly_score: { - value: number | null; - }; - lower: { - value: number | null; - }; - upper: { - value: number | null; - }; -} - -interface Aggs { - ml_avg_response_times: { - buckets: ESBucket[]; - }; -} +export type ESResponse = Exclude< + PromiseReturnType, + undefined +>; -export type ESResponse = PromiseReturnType; export async function anomalySeriesFetcher({ serviceName, transactionType, @@ -91,7 +74,8 @@ export async function anomalySeriesFetcher({ }; try { - return await client.search(params); + const response = await client.search(params); + return response; } catch (err) { const isHttpError = 'statusCode' in err; if (isHttpError) { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index f3227140c692b..7b5b77e2a2ddc 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -55,10 +55,12 @@ export async function getAnomalySeries({ setup }); - return anomalySeriesTransform( - esResponse, - mlBucketSize, - bucketSize, - timeSeriesDates - ); + return esResponse + ? anomalySeriesTransform( + esResponse, + mlBucketSize, + bucketSize, + timeSeriesDates + ) + : undefined; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlAnomalyResponse.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlAnomalyResponse.ts index eaea107e5ef2d..c04cf95526d4b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlAnomalyResponse.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlAnomalyResponse.ts @@ -6,7 +6,7 @@ import { ESResponse } from '../fetcher'; -export const mlAnomalyResponse: ESResponse = { +export const mlAnomalyResponse: ESResponse = ({ took: 3, timed_out: false, _shards: { @@ -124,4 +124,4 @@ export const mlAnomalyResponse: ESResponse = { ] } } -}; +} as unknown) as ESResponse; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts index bbcab297f3a93..eab68a2bda974 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts @@ -5,7 +5,7 @@ */ import { idx } from '@kbn/elastic-idx'; -import { ESBucket, ESResponse } from './fetcher'; +import { ESResponse } from './fetcher'; import { mlAnomalyResponse } from './mock-responses/mlAnomalyResponse'; import { anomalySeriesTransform, replaceFirstAndLastBucket } from './transform'; @@ -46,7 +46,7 @@ describe('anomalySeriesTransform', () => { key: 20000, anomaly_score: { value: 90 } } - ] as ESBucket[]); + ]); const getMlBucketSize = 5; const bucketSize = 5; @@ -72,7 +72,7 @@ describe('anomalySeriesTransform', () => { key: 5000, anomaly_score: { value: 90 } } - ] as ESBucket[]); + ]); const getMlBucketSize = 10; const bucketSize = 5; @@ -112,7 +112,7 @@ describe('anomalySeriesTransform', () => { upper: { value: 45 }, lower: { value: 40 } } - ] as ESBucket[]); + ]); const mlBucketSize = 10; const bucketSize = 5; @@ -151,7 +151,7 @@ describe('anomalySeriesTransform', () => { upper: { value: 25 }, lower: { value: 20 } } - ] as ESBucket[]); + ]); const getMlBucketSize = 10; const bucketSize = 5; @@ -190,7 +190,7 @@ describe('anomalySeriesTransform', () => { upper: { value: null }, lower: { value: null } } - ] as ESBucket[]); + ]); const getMlBucketSize = 10; const bucketSize = 5; @@ -234,10 +234,10 @@ describe('replaceFirstAndLastBucket', () => { lower: 30, upper: 40 } - ] as any; + ]; const timeSeriesDates = [10, 15]; - expect(replaceFirstAndLastBucket(buckets, timeSeriesDates)).toEqual([ + expect(replaceFirstAndLastBucket(buckets as any, timeSeriesDates)).toEqual([ { x: 10, lower: 10, upper: 20 }, { x: 15, lower: 30, upper: 40 } ]); @@ -271,8 +271,8 @@ describe('replaceFirstAndLastBucket', () => { }); }); -function getESResponse(buckets: ESBucket[]): ESResponse { - return { +function getESResponse(buckets: any): ESResponse { + return ({ took: 3, timed_out: false, _shards: { @@ -288,7 +288,7 @@ function getESResponse(buckets: ESBucket[]): ESResponse { }, aggregations: { ml_avg_response_times: { - buckets: buckets.map(bucket => { + buckets: buckets.map((bucket: any) => { return { ...bucket, lower: { value: idx(bucket, _ => _.lower.value) || null }, @@ -300,5 +300,5 @@ function getESResponse(buckets: ESBucket[]): ESResponse { }) } } - }; + } as unknown) as ESResponse; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts index 211918fc2417d..27cb122addfb1 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts @@ -7,10 +7,12 @@ import { first, last } from 'lodash'; import { idx } from '@kbn/elastic-idx'; import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries'; -import { ESBucket, ESResponse } from './fetcher'; +import { ESResponse } from './fetcher'; type IBucket = ReturnType; -function getBucket(bucket: ESBucket) { +function getBucket( + bucket: ESResponse['aggregations']['ml_avg_response_times']['buckets'][0] +) { return { x: bucket.key, anomalyScore: bucket.anomaly_score.value, @@ -28,10 +30,6 @@ export function anomalySeriesTransform( bucketSize: number, timeSeriesDates: number[] ) { - if (!response) { - return; - } - const buckets = ( idx(response, _ => _.aggregations.ml_avg_response_times.buckets) || [] ).map(getBucket); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index ceb19b865177d..8ccf1af568535 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter, SearchParams } from 'elasticsearch'; +import { ESFilter } from 'elasticsearch'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -18,53 +18,6 @@ import { getBucketSize } from '../../../helpers/get_bucket_size'; import { rangeFilter } from '../../../helpers/range_filter'; import { Setup } from '../../../helpers/setup_request'; -interface ResponseTimeBucket { - key_as_string: string; - key: number; - doc_count: number; - avg: { - value: number | null; - }; - pct: { - values: { - '95.0': number | 'NaN'; - '99.0': number | 'NaN'; - }; - }; -} - -interface TransactionResultBucket { - /** - * transaction result eg. 2xx - */ - key: string; - doc_count: number; - timeseries: { - buckets: Array<{ - key_as_string: string; - /** - * timestamp in ms - */ - key: number; - doc_count: number; - }>; - }; -} - -interface Aggs { - response_times: { - buckets: ResponseTimeBucket[]; - }; - transaction_results: { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: TransactionResultBucket[]; - }; - overall_avg_duration: { - value: number; - }; -} - export type ESResponse = PromiseReturnType; export function timeseriesFetcher({ serviceName, @@ -96,8 +49,8 @@ export function timeseriesFetcher({ filter.push({ term: { [TRANSACTION_TYPE]: transactionType } }); } - const params: SearchParams = { - index: config.get('apm_oss.transactionIndices'), + const params = { + index: config.get('apm_oss.transactionIndices'), body: { size: 0, query: { bool: { filter } }, @@ -134,5 +87,5 @@ export function timeseriesFetcher({ } }; - return client.search(params); + return client.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock-responses/timeseries_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock-responses/timeseries_response.ts index 075ede23fb38e..8f70f007a3f1d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock-responses/timeseries_response.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/mock-responses/timeseries_response.ts @@ -6,7 +6,7 @@ import { ESResponse } from '../fetcher'; -export const timeseriesResponse: ESResponse = { +export const timeseriesResponse = ({ took: 368, timed_out: false, _shards: { @@ -2826,4 +2826,4 @@ export const timeseriesResponse: ESResponse = { value: 32861.15660262639 } } -}; +} as unknown) as ESResponse; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts index a584cd70e2f8b..1da800ae21ab2 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.test.ts @@ -96,7 +96,7 @@ describe('getTpmBuckets', () => { } ]; const bucketSize = 10; - expect(getTpmBuckets(buckets, bucketSize)).toEqual([ + expect(getTpmBuckets(buckets as any, bucketSize)).toEqual([ { dataPoints: [ { x: 0, y: 0 }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts index 6d639eb7f9ffd..e3978217f260b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform.ts @@ -10,19 +10,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate } from '../../../../../typings/timeseries'; import { ESResponse } from './fetcher'; -export interface ApmTimeSeriesResponse { - totalHits: number; - responseTimes: { - avg: Coordinate[]; - p95: Coordinate[]; - p99: Coordinate[]; - }; - tpmBuckets: Array<{ - key: string; - dataPoints: Coordinate[]; - }>; - overallAvgDuration?: number; -} +export type ApmTimeSeriesResponse = ReturnType; export function timeseriesTransformer({ timeseriesResponse, @@ -30,7 +18,7 @@ export function timeseriesTransformer({ }: { timeseriesResponse: ESResponse; bucketSize: number; -}): ApmTimeSeriesResponse { +}) { const aggs = timeseriesResponse.aggregations; const overallAvgDuration = idx(aggs, _ => _.overall_avg_duration.value); const responseTimeBuckets = idx(aggs, _ => _.response_times.buckets); diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts index 3e2a285f864e9..20f69a0bd4d8c 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/calculate_bucket_size.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchParams } from 'elasticsearch'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -22,8 +21,8 @@ export async function calculateBucketSize( ) { const { start, end, uiFiltersES, client, config } = setup; - const params: SearchParams = { - index: config.get('apm_oss.transactionIndices'), + const params = { + index: config.get('apm_oss.transactionIndices'), body: { size: 0, query: { @@ -56,13 +55,8 @@ export async function calculateBucketSize( } }; - interface Aggs { - stats: { - max: number; - }; - } + const resp = await client.search(params); - const resp = await client.search(params); const minBucketSize: number = config.get('xpack.apm.minimumBucketSize'); const bucketTargetCount: number = config.get('xpack.apm.bucketTargetCount'); const max = resp.aggregations.stats.max; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts index e67d7db075df1..d265aa5173d2f 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/fetcher.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -15,29 +14,9 @@ import { TRANSACTION_SAMPLED, TRANSACTION_TYPE } from '../../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../../typings/common'; -import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; import { rangeFilter } from '../../../helpers/range_filter'; import { Setup } from '../../../helpers/setup_request'; -interface Bucket { - key: number; - doc_count: number; - sample: SearchResponse<{ - transaction: Pick; - trace: { - id: string; - }; - }>; -} - -interface Aggs { - distribution: { - buckets: Bucket[]; - }; -} - -export type ESResponse = PromiseReturnType; export function bucketFetcher( serviceName: string, transactionName: string, @@ -95,5 +74,5 @@ export function bucketFetcher( } }; - return client.search(params); + return client.search(params); } diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts index 07e5004655e7f..c17b388cabb19 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts @@ -6,7 +6,11 @@ import { isEmpty } from 'lodash'; import { idx } from '@kbn/elastic-idx'; -import { ESResponse } from './fetcher'; +import { PromiseReturnType } from '../../../../../typings/common'; +import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; +import { bucketFetcher } from './fetcher'; + +type DistributionBucketResponse = PromiseReturnType; function getDefaultSample(buckets: IBucket[]) { const samples = buckets @@ -23,9 +27,12 @@ function getDefaultSample(buckets: IBucket[]) { export type IBucket = ReturnType; function getBucket( - bucket: ESResponse['aggregations']['distribution']['buckets'][0] + bucket: DistributionBucketResponse['aggregations']['distribution']['buckets'][0] ) { - const sampleSource = idx(bucket, _ => _.sample.hits.hits[0]._source); + const sampleSource = idx(bucket, _ => _.sample.hits.hits[0]._source) as + | Transaction + | undefined; + const isSampled = idx(sampleSource, _ => _.transaction.sampled); const sample = { traceId: idx(sampleSource, _ => _.trace.id), @@ -39,7 +46,7 @@ function getBucket( }; } -export function bucketTransformer(response: ESResponse) { +export function bucketTransformer(response: DistributionBucketResponse) { const buckets = response.aggregations.distribution.buckets.map(getBucket); return { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts index 71658da91bb37..93a82bf6db85b 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BucketAgg, ESFilter } from 'elasticsearch'; +import { ESFilter } from 'elasticsearch'; import { idx } from '@kbn/elastic-idx'; import { PROCESSOR_EVENT, @@ -57,13 +57,7 @@ export async function getEnvironments(setup: Setup, serviceName?: string) { } }; - interface Aggs extends BucketAgg { - environments: { - buckets: BucketAgg[]; - }; - } - - const resp = await client.search(params); + const resp = await client.search(params); const aggs = resp.aggregations; const environmentsBuckets = idx(aggs, _ => _.environments.buckets) || []; diff --git a/x-pack/plugins/apm/typings/common.ts b/x-pack/plugins/apm/typings/common.ts index 6abbcc5413bd2..42c9d90f809aa 100644 --- a/x-pack/plugins/apm/typings/common.ts +++ b/x-pack/plugins/apm/typings/common.ts @@ -20,3 +20,9 @@ export type PromiseReturnType = Func extends ( ) => Promise ? Value : Func; + +export type IndexAsString = { + [k: string]: Map[keyof Map]; +} & Map; + +export type Omit = Pick>; diff --git a/x-pack/plugins/apm/typings/elasticsearch.ts b/x-pack/plugins/apm/typings/elasticsearch.ts index 1fa52da1e26da..4ba936f480155 100644 --- a/x-pack/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/plugins/apm/typings/elasticsearch.ts @@ -4,25 +4,115 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StringMap } from './common'; +import { StringMap, IndexAsString } from './common'; declare module 'elasticsearch' { // extending SearchResponse to be able to have typed aggregations - export interface AggregationSearchResponse - extends SearchResponse { - aggregations: Aggs; - } - export interface BucketAgg { - key: T; - doc_count: number; - } + type AggregationType = + | 'date_histogram' + | 'histogram' + | 'terms' + | 'avg' + | 'top_hits' + | 'max' + | 'min' + | 'percentiles' + | 'sum' + | 'extended_stats'; + + type AggOptions = AggregationOptionMap & { + [key: string]: any; + }; + + // eslint-disable-next-line @typescript-eslint/prefer-interface + export type AggregationOptionMap = { + aggs?: { + [aggregationName: string]: { + [T in AggregationType]?: AggOptions & AggregationOptionMap + }; + }; + }; + + // eslint-disable-next-line @typescript-eslint/prefer-interface + type BucketAggregation = { + buckets: Array< + { + key: KeyType; + key_as_string: string; + doc_count: number; + } & (SubAggregationMap extends { aggs: any } + ? AggregationResultMap + : {}) + >; + }; - export interface TermsAggsBucket { - key: string; - doc_count: number; + interface AggregatedValue { + value: number | null; } + type AggregationResultMap = IndexAsString< + { + [AggregationName in keyof AggregationOption]: { + avg: AggregatedValue; + max: AggregatedValue; + min: AggregatedValue; + sum: AggregatedValue; + terms: BucketAggregation; + date_histogram: BucketAggregation< + AggregationOption[AggregationName], + number + >; + histogram: BucketAggregation< + AggregationOption[AggregationName], + number + >; + top_hits: { + hits: { + total: number; + max_score: number | null; + hits: Array<{ + _source: AggregationOption[AggregationName] extends { + Mapping: any; + } + ? AggregationOption[AggregationName]['Mapping'] + : never; + }>; + }; + }; + percentiles: { + values: { + [key: string]: number; + }; + }; + extended_stats: { + count: number; + min: number; + max: number; + avg: number; + sum: number; + sum_of_squares: number; + variance: number; + std_deviation: number; + std_deviation_bounds: { + upper: number; + lower: number; + }; + }; + }[AggregationType & keyof AggregationOption[AggregationName]] + } + >; + + export type AggregationSearchResponse = Pick< + SearchResponse, + Exclude, 'aggregations'> + > & + (SearchParams extends { body: Required } + ? { + aggregations: AggregationResultMap; + } + : {}); + export interface ESFilter { [key: string]: { [key: string]: string | number | StringMap | ESFilter[]; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts index e0722605535a2..735efe73aed10 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/TransactionRaw.ts @@ -14,6 +14,7 @@ import { Process } from './fields/Process'; import { Service } from './fields/Service'; import { Url } from './fields/Url'; import { User } from './fields/User'; +import { UserAgent } from './fields/UserAgent'; interface Processor { name: 'transaction'; @@ -52,4 +53,5 @@ export interface TransactionRaw extends APMBaseDoc { service: Service; url?: Url; user?: User; + user_agent?: UserAgent; } diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/fields/UserAgent.ts b/x-pack/plugins/apm/typings/es_schemas/raw/fields/UserAgent.ts new file mode 100644 index 0000000000000..af88628119b58 --- /dev/null +++ b/x-pack/plugins/apm/typings/es_schemas/raw/fields/UserAgent.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface UserAgent { + device: { + name: string; + }; + name: string; + original: string; + os?: { + name: string; + version: string; + full: string; + }; + version?: string; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts similarity index 96% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts index 7c58bb53bc367..1a535ceacbf5d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -const emptyTable = { +import { Datatable } from '../../../types'; + +const emptyTable: Datatable = { type: 'datatable', columns: [], rows: [], }; -const testTable = { +const testTable: Datatable = { type: 'datatable', columns: [ { @@ -101,7 +103,7 @@ const testTable = { ], }; -const stringTable = { +const stringTable: Datatable = { type: 'datatable', columns: [ { diff --git a/x-pack/plugins/canvas/common/lib/__tests__/find_in_object.js b/x-pack/plugins/canvas/common/lib/__tests__/find_in_object.js deleted file mode 100644 index f3fa5ebb81a14..0000000000000 --- a/x-pack/plugins/canvas/common/lib/__tests__/find_in_object.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { findInObject } from '../find_in_object'; - -const findMe = { - foo: { - baz: { - id: 0, - bar: 5, - }, - beer: { - id: 1, - bar: 10, - }, - thing: { - id: 4, - stuff: { - id: 3, - dude: [ - { - bar: 5, - id: 2, - }, - ], - baz: { - bar: 5, - id: 7, - }, - nice: { - bar: 5, - id: 8, - }, - thing: [], - thing2: [[[[]], { bar: 5, id: 6 }]], - }, - }, - }, -}; - -describe('findInObject', () => { - it('Finds object matching a function', () => { - expect(findInObject(findMe, obj => obj.bar === 5).length).to.eql(5); - expect(findInObject(findMe, obj => obj.bar === 5)[0].id).to.eql(0); - expect(findInObject(findMe, obj => obj.bar === 5)[1].id).to.eql(2); - - expect(findInObject(findMe, obj => obj.id === 4).length).to.eql(1); - expect(findInObject(findMe, obj => obj.id === 10000).length).to.eql(0); - }); -}); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/get_field_type.js b/x-pack/plugins/canvas/common/lib/__tests__/get_field_type.test.ts similarity index 53% rename from x-pack/plugins/canvas/common/lib/__tests__/get_field_type.js rename to x-pack/plugins/canvas/common/lib/__tests__/get_field_type.test.ts index 974880e74e328..34cfbb5a2befb 100644 --- a/x-pack/plugins/canvas/common/lib/__tests__/get_field_type.js +++ b/x-pack/plugins/canvas/common/lib/__tests__/get_field_type.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { getFieldType } from '../get_field_type'; import { emptyTable, @@ -13,14 +12,14 @@ import { describe('getFieldType', () => { it('returns type of a field in a datatable', () => { - expect(getFieldType(testTable.columns, 'name')).to.be('string'); - expect(getFieldType(testTable.columns, 'time')).to.be('date'); - expect(getFieldType(testTable.columns, 'price')).to.be('number'); - expect(getFieldType(testTable.columns, 'quantity')).to.be('number'); - expect(getFieldType(testTable.columns, 'in_stock')).to.be('boolean'); + expect(getFieldType(testTable.columns, 'name')).toBe('string'); + expect(getFieldType(testTable.columns, 'time')).toBe('date'); + expect(getFieldType(testTable.columns, 'price')).toBe('number'); + expect(getFieldType(testTable.columns, 'quantity')).toBe('number'); + expect(getFieldType(testTable.columns, 'in_stock')).toBe('boolean'); }); it(`returns 'null' if field does not exist in datatable`, () => { - expect(getFieldType(testTable.columns, 'foo')).to.be('null'); - expect(getFieldType(emptyTable.columns, 'foo')).to.be('null'); + expect(getFieldType(testTable.columns, 'foo')).toBe('null'); + expect(getFieldType(emptyTable.columns, 'foo')).toBe('null'); }); }); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/httpurl.js b/x-pack/plugins/canvas/common/lib/__tests__/httpurl.test.ts similarity index 73% rename from x-pack/plugins/canvas/common/lib/__tests__/httpurl.js rename to x-pack/plugins/canvas/common/lib/__tests__/httpurl.test.ts index cefe2530a01f9..2a7cef7cf4236 100644 --- a/x-pack/plugins/canvas/common/lib/__tests__/httpurl.js +++ b/x-pack/plugins/canvas/common/lib/__tests__/httpurl.test.ts @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { isValidHttpUrl } from '../httpurl'; describe('httpurl.isValidHttpUrl', () => { it('matches HTTP URLs', () => { - expect(isValidHttpUrl('http://server.com/veggie/hamburger.jpg')).to.be(true); - expect(isValidHttpUrl('https://server.com:4443/veggie/hamburger.jpg')).to.be(true); - expect(isValidHttpUrl('http://user:password@server.com:4443/veggie/hamburger.jpg')).to.be(true); - expect(isValidHttpUrl('http://virtual-machine/veggiehamburger.jpg')).to.be(true); - expect(isValidHttpUrl('https://virtual-machine:44330/veggie.jpg?hamburger')).to.be(true); - expect(isValidHttpUrl('http://192.168.1.50/veggie/hamburger.jpg')).to.be(true); - expect(isValidHttpUrl('https://2600::/veggie/hamburger.jpg')).to.be(true); // ipv6 - expect(isValidHttpUrl('http://2001:4860:4860::8844/veggie/hamburger.jpg')).to.be(true); // ipv6 + expect(isValidHttpUrl('http://server.com/veggie/hamburger.jpg')).toBe(true); + expect(isValidHttpUrl('https://server.com:4443/veggie/hamburger.jpg')).toBe(true); + expect(isValidHttpUrl('http://user:password@server.com:4443/veggie/hamburger.jpg')).toBe(true); + expect(isValidHttpUrl('http://virtual-machine/veggiehamburger.jpg')).toBe(true); + expect(isValidHttpUrl('https://virtual-machine:44330/veggie.jpg?hamburger')).toBe(true); + expect(isValidHttpUrl('http://192.168.1.50/veggie/hamburger.jpg')).toBe(true); + expect(isValidHttpUrl('https://2600::/veggie/hamburger.jpg')).toBe(true); // ipv6 + expect(isValidHttpUrl('http://2001:4860:4860::8844/veggie/hamburger.jpg')).toBe(true); // ipv6 }); it('rejects non-HTTP URLs', () => { - expect(isValidHttpUrl('')).to.be(false); - expect(isValidHttpUrl('http://server.com')).to.be(false); - expect(isValidHttpUrl('file:///Users/programmer/Pictures/hamburger.jpeg')).to.be(false); - expect(isValidHttpUrl('ftp://hostz.com:1111/path/to/image.png')).to.be(false); - expect(isValidHttpUrl('ftp://user:password@host:1111/path/to/image.png')).to.be(false); + expect(isValidHttpUrl('')).toBe(false); + expect(isValidHttpUrl('http://server.com')).toBe(false); + expect(isValidHttpUrl('file:///Users/programmer/Pictures/hamburger.jpeg')).toBe(false); + expect(isValidHttpUrl('ftp://hostz.com:1111/path/to/image.png')).toBe(false); + expect(isValidHttpUrl('ftp://user:password@host:1111/path/to/image.png')).toBe(false); expect( isValidHttpUrl('data:image/gif;base64,R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+...') - ).to.be(false); + ).toBe(false); }); }); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/latest_change.js b/x-pack/plugins/canvas/common/lib/__tests__/latest_change.js deleted file mode 100644 index f3678267540bf..0000000000000 --- a/x-pack/plugins/canvas/common/lib/__tests__/latest_change.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { latestChange } from '../latest_change'; - -describe('latestChange', () => { - it('returns a function', () => { - const checker = latestChange(); - expect(checker).to.be.a('function'); - }); - - it('returns null without args', () => { - const checker = latestChange(); - expect(checker()).to.be(null); - }); - - describe('checker function', () => { - let checker; - - beforeEach(() => { - checker = latestChange(1, 2, 3); - }); - - it('returns null if nothing changed', () => { - expect(checker(1, 2, 3)).to.be(null); - }); - - it('returns the latest value', () => { - expect(checker(1, 4, 3)).to.equal(4); - }); - - it('returns the newst value every time', () => { - expect(checker(1, 4, 3)).to.equal(4); - expect(checker(10, 4, 3)).to.equal(10); - expect(checker(10, 4, 30)).to.equal(30); - }); - - it('returns the previous value if nothing changed', () => { - expect(checker(1, 4, 3)).to.equal(4); - expect(checker(1, 4, 3)).to.equal(4); - }); - - it('returns only the first changed value', () => { - expect(checker(2, 4, 3)).to.equal(2); - expect(checker(2, 10, 11)).to.equal(10); - }); - - it('does not check new arguments', () => { - // 4th arg is new, so nothing changed compared to the first state - expect(checker(1, 2, 3, 4)).to.be(null); - expect(checker(1, 2, 3, 5)).to.be(null); - expect(checker(1, 2, 3, 6)).to.be(null); - }); - - it('returns changes with too many args', () => { - expect(checker(20, 2, 3, 4)).to.equal(20); - expect(checker(20, 2, 30, 5)).to.equal(30); - }); - - it('returns undefined values', () => { - expect(checker(1, undefined, 3, 4)).to.be(undefined); - }); - }); -}); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/pivot_object_array.js b/x-pack/plugins/canvas/common/lib/__tests__/pivot_object_array.test.ts similarity index 50% rename from x-pack/plugins/canvas/common/lib/__tests__/pivot_object_array.js rename to x-pack/plugins/canvas/common/lib/__tests__/pivot_object_array.test.ts index 1ef875feacdc6..6f6d42e7129a9 100644 --- a/x-pack/plugins/canvas/common/lib/__tests__/pivot_object_array.js +++ b/x-pack/plugins/canvas/common/lib/__tests__/pivot_object_array.test.ts @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { pivotObjectArray } from '../pivot_object_array'; +interface Car { + make: string; + model: string; + price: string; +} + describe('pivotObjectArray', () => { - let rows; + let rows: Car[] = []; beforeEach(() => { rows = [ @@ -19,37 +24,39 @@ describe('pivotObjectArray', () => { }); it('converts array of objects', () => { - const data = pivotObjectArray(rows); + const data = pivotObjectArray(rows); + + expect(typeof data).toBe('object'); - expect(data).to.be.an('object'); - expect(data).to.have.property('make'); - expect(data).to.have.property('model'); - expect(data).to.have.property('price'); + expect(data).toHaveProperty('make'); + expect(data).toHaveProperty('model'); + expect(data).toHaveProperty('price'); - expect(data.make).to.eql(['honda', 'toyota', 'tesla']); - expect(data.model).to.eql(['civic', 'corolla', 'model 3']); - expect(data.price).to.eql(['10000', '12000', '35000']); + expect(data.make).toEqual(['honda', 'toyota', 'tesla']); + expect(data.model).toEqual(['civic', 'corolla', 'model 3']); + expect(data.price).toEqual(['10000', '12000', '35000']); }); it('uses passed in column list', () => { - const data = pivotObjectArray(rows, ['price']); + const data = pivotObjectArray(rows, ['price']); - expect(data).to.be.an('object'); - expect(data).to.eql({ price: ['10000', '12000', '35000'] }); + expect(typeof data).toBe('object'); + expect(data).toEqual({ price: ['10000', '12000', '35000'] }); }); it('adds missing columns with undefined values', () => { - const data = pivotObjectArray(rows, ['price', 'missing']); + const data = pivotObjectArray(rows, ['price', 'missing']); - expect(data).to.be.an('object'); - expect(data).to.eql({ + expect(typeof data).toBe('object'); + expect(data).toEqual({ price: ['10000', '12000', '35000'], missing: [undefined, undefined, undefined], }); }); it('throws when given an invalid column list', () => { + // @ts-ignore testing potential calls from legacy code that should throw const check = () => pivotObjectArray(rows, [{ name: 'price' }, { name: 'missing' }]); - expect(check).to.throwException('Columns should be an array of strings'); + expect(check).toThrowError('Columns should be an array of strings'); }); }); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/unquote_string.js b/x-pack/plugins/canvas/common/lib/__tests__/unquote_string.js deleted file mode 100644 index 52fcabf5ae7e0..0000000000000 --- a/x-pack/plugins/canvas/common/lib/__tests__/unquote_string.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { unquoteString } from '../unquote_string'; - -describe('unquoteString', () => { - it('removes double quotes', () => { - expect(unquoteString('"hello world"')).to.equal('hello world'); - }); - - it('removes single quotes', () => { - expect(unquoteString("'hello world'")).to.equal('hello world'); - }); - - it('returns unquoted strings', () => { - expect(unquoteString('hello world')).to.equal('hello world'); - expect(unquoteString('hello')).to.equal('hello'); - expect(unquoteString('hello"world')).to.equal('hello"world'); - expect(unquoteString("hello'world")).to.equal("hello'world"); - expect(unquoteString("'hello'world")).to.equal("'hello'world"); - expect(unquoteString('"hello"world')).to.equal('"hello"world'); - }); -}); diff --git a/x-pack/plugins/canvas/common/lib/__tests__/unquote_string.test.ts b/x-pack/plugins/canvas/common/lib/__tests__/unquote_string.test.ts new file mode 100644 index 0000000000000..e67e46f9e5dac --- /dev/null +++ b/x-pack/plugins/canvas/common/lib/__tests__/unquote_string.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { unquoteString } from '../unquote_string'; + +describe('unquoteString', () => { + it('removes double quotes', () => { + expect(unquoteString('"hello world"')).toEqual('hello world'); + }); + + it('removes single quotes', () => { + expect(unquoteString("'hello world'")).toEqual('hello world'); + }); + + it('returns unquoted strings', () => { + expect(unquoteString('hello world')).toEqual('hello world'); + expect(unquoteString('hello')).toEqual('hello'); + expect(unquoteString('hello"world')).toEqual('hello"world'); + expect(unquoteString("hello'world")).toEqual("hello'world"); + expect(unquoteString("'hello'world")).toEqual("'hello'world"); + expect(unquoteString('"hello"world')).toEqual('"hello"world'); + }); +}); diff --git a/x-pack/plugins/canvas/common/lib/find_in_object.js b/x-pack/plugins/canvas/common/lib/find_in_object.js deleted file mode 100644 index 484f718fd81c1..0000000000000 --- a/x-pack/plugins/canvas/common/lib/find_in_object.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { each } from 'lodash'; - -export function findInObject(o, fn, memo, name) { - memo = memo || []; - if (fn(o, name)) { - memo.push(o); - } - if (o != null && typeof o === 'object') { - each(o, (val, name) => findInObject(val, fn, memo, name)); - } - - return memo; -} diff --git a/x-pack/plugins/canvas/common/lib/get_field_type.js b/x-pack/plugins/canvas/common/lib/get_field_type.js deleted file mode 100644 index 1f326b1042ec1..0000000000000 --- a/x-pack/plugins/canvas/common/lib/get_field_type.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { unquoteString } from './unquote_string'; - -export function getFieldType(columns, field) { - if (!field) { - return 'null'; - } - const realField = unquoteString(field); - const column = columns.find(column => column.name === realField); - return column ? column.type : 'null'; -} diff --git a/x-pack/plugins/canvas/common/lib/get_field_type.ts b/x-pack/plugins/canvas/common/lib/get_field_type.ts new file mode 100644 index 0000000000000..f46f8a84264b1 --- /dev/null +++ b/x-pack/plugins/canvas/common/lib/get_field_type.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DatatableColumn } from '../../canvas_plugin_src/functions/types'; +import { unquoteString } from './unquote_string'; + +/** + * Get the type for the column with the given name + * + * @argument columns Array of all columns + * @field Name of the column that we are looking for the type of + * @returns The column type or the string 'null' + */ +export function getFieldType(columns: DatatableColumn[], field?: string): string { + if (!field) { + return 'null'; + } + const realField = unquoteString(field); + const column = columns.find(dataTableColumn => dataTableColumn.name === realField); + return column ? column.type : 'null'; +} diff --git a/x-pack/plugins/canvas/common/lib/httpurl.js b/x-pack/plugins/canvas/common/lib/httpurl.ts similarity index 88% rename from x-pack/plugins/canvas/common/lib/httpurl.js rename to x-pack/plugins/canvas/common/lib/httpurl.ts index 3902395fec3d6..71e482777ec0f 100644 --- a/x-pack/plugins/canvas/common/lib/httpurl.js +++ b/x-pack/plugins/canvas/common/lib/httpurl.ts @@ -7,6 +7,6 @@ // A cheap regex to distinguish an HTTP URL string from a data URL string const httpurlRegex = /^https?:\/\/\S+(?:[0-9]+)?\/\S{1,}/; -export function isValidHttpUrl(str) { +export function isValidHttpUrl(str: string): boolean { return httpurlRegex.test(str); } diff --git a/x-pack/plugins/canvas/common/lib/index.ts b/x-pack/plugins/canvas/common/lib/index.ts index e7f901d681fbd..5ab29c290c3da 100644 --- a/x-pack/plugins/canvas/common/lib/index.ts +++ b/x-pack/plugins/canvas/common/lib/index.ts @@ -16,8 +16,6 @@ export * from './errors'; export * from './expression_form_handlers'; // @ts-ignore missing local definition export * from './fetch'; -// @ts-ignore missing local definition -export * from './find_in_object'; export * from './fonts'; // @ts-ignore missing local definition export * from './get_colors_from_palette'; @@ -31,8 +29,6 @@ export * from './hex_to_rgb'; // @ts-ignore missing local definition export * from './httpurl'; // @ts-ignore missing local definition -export * from './latest_change'; -// @ts-ignore missing local definition export * from './missing_asset'; // @ts-ignore missing local definition export * from './palettes'; diff --git a/x-pack/plugins/canvas/common/lib/latest_change.js b/x-pack/plugins/canvas/common/lib/latest_change.js deleted file mode 100644 index 3839481dcfabf..0000000000000 --- a/x-pack/plugins/canvas/common/lib/latest_change.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function latestChange(...firstArgs) { - let oldState = firstArgs; - let prevValue = null; - - return (...args) => { - let found = false; - - const newState = oldState.map((oldVal, i) => { - const val = args[i]; - if (!found && oldVal !== val) { - found = true; - prevValue = val; - } - return val; - }); - - oldState = newState; - - return prevValue; - }; -} diff --git a/x-pack/plugins/canvas/common/lib/pivot_object_array.js b/x-pack/plugins/canvas/common/lib/pivot_object_array.ts similarity index 61% rename from x-pack/plugins/canvas/common/lib/pivot_object_array.js rename to x-pack/plugins/canvas/common/lib/pivot_object_array.ts index ca191ece3ce05..f13a2a2af8844 100644 --- a/x-pack/plugins/canvas/common/lib/pivot_object_array.js +++ b/x-pack/plugins/canvas/common/lib/pivot_object_array.ts @@ -6,9 +6,15 @@ import { map, zipObject } from 'lodash'; -const isString = val => typeof val === 'string'; +const isString = (val: any): boolean => typeof val === 'string'; -export function pivotObjectArray(rows, columns) { +export function pivotObjectArray< + RowType extends { [key: string]: any }, + ReturnColumns extends string | number | symbol = keyof RowType +>( + rows: RowType[], + columns?: string[] +): { [Column in ReturnColumns]: Column extends keyof RowType ? Array : never } { const columnNames = columns || Object.keys(rows[0]); if (!columnNames.every(isString)) { throw new Error('Columns should be an array of strings'); diff --git a/x-pack/plugins/canvas/common/lib/unquote_string.js b/x-pack/plugins/canvas/common/lib/unquote_string.ts similarity index 66% rename from x-pack/plugins/canvas/common/lib/unquote_string.js rename to x-pack/plugins/canvas/common/lib/unquote_string.ts index 0d6a31004d0b1..bb1edab4c038c 100644 --- a/x-pack/plugins/canvas/common/lib/unquote_string.js +++ b/x-pack/plugins/canvas/common/lib/unquote_string.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export const unquoteString = str => { +/** + * Removes single or double quotes if any exist around the given string + * @param str the string to unquote + * @returns the unquoted string + */ +export const unquoteString = (str: string): string => { if (/^"/.test(str)) { return str.replace(/^"(.+(?="$))"$/, '$1'); } diff --git a/x-pack/plugins/code/common/lsp_error_codes.ts b/x-pack/plugins/code/common/lsp_error_codes.ts index b51361b06364d..6a2ad895da942 100644 --- a/x-pack/plugins/code/common/lsp_error_codes.ts +++ b/x-pack/plugins/code/common/lsp_error_codes.ts @@ -8,6 +8,7 @@ import { ErrorCodes } from 'vscode-jsonrpc/lib/messages'; export const ServerNotInitialized: number = ErrorCodes.ServerNotInitialized; export const UnknownErrorCode: number = ErrorCodes.UnknownErrorCode; +export const RequestCancelled: number = ErrorCodes.RequestCancelled; export const UnknownFileLanguage: number = -42404; export const LanguageServerNotInstalled: number = -42403; export const LanguageDisabled: number = -42402; diff --git a/x-pack/plugins/code/public/lib/documentation_links.ts b/x-pack/plugins/code/public/lib/documentation_links.ts index d9181d950946b..34d29a8150c78 100644 --- a/x-pack/plugins/code/public/lib/documentation_links.ts +++ b/x-pack/plugins/code/public/lib/documentation_links.ts @@ -11,11 +11,11 @@ export const documentationLinks = { code: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code.html`, codeIntelligence: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code.html`, gitFormat: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code.html`, - codeInstallLangServer: `${ELASTIC_WEBSITE_URL}guide/en/kibana/7.2/code-install-language-server.html`, - codeGettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/kibana/7.2/code-getting-started.html`, - codeRepoManagement: `${ELASTIC_WEBSITE_URL}guide/en/kibana/7.2/code-repo-management.html`, - codeSearch: `${ELASTIC_WEBSITE_URL}guide/en/kibana/7.2/code-search.html`, - codeOtherFeatures: `${ELASTIC_WEBSITE_URL}guide/en/kibana/7.2/code-basic-nav.html`, - semanticNavigation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/7.2/code-semantic-nav.html`, - kibanaRoleManagement: `${ELASTIC_WEBSITE_URL}guide/en/kibana/7.2/_kibana_role_management.html`, + codeInstallLangServer: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-install-language-server.html`, + codeGettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-getting-started.html`, + codeRepoManagement: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-repo-management.html`, + codeSearch: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-search.html`, + codeOtherFeatures: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-basic-nav.html`, + semanticNavigation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/code-semantic-nav.html`, + kibanaRoleManagement: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/_kibana_role_management.html`, }; diff --git a/x-pack/plugins/code/public/monaco/blame/blame_widget.ts b/x-pack/plugins/code/public/monaco/blame/blame_widget.ts index cf3b5993f21e3..ca93a3e865de6 100644 --- a/x-pack/plugins/code/public/monaco/blame/blame_widget.ts +++ b/x-pack/plugins/code/public/monaco/blame/blame_widget.ts @@ -56,6 +56,7 @@ export class BlameWidget implements Editor.IContentWidget { } private update() { + this.containerNode.style.width = '0px'; const { fontSize, lineHeight } = this.editor.getConfiguration().fontInfo; this.domNode.style.position = 'relative'; this.domNode.style.left = '-332px'; diff --git a/x-pack/plugins/code/public/reducers/__tests__/match_container_name.test.ts b/x-pack/plugins/code/public/reducers/__tests__/match_container_name.test.ts index 64d8553d0f91e..efc4be14286b4 100644 --- a/x-pack/plugins/code/public/reducers/__tests__/match_container_name.test.ts +++ b/x-pack/plugins/code/public/reducers/__tests__/match_container_name.test.ts @@ -7,6 +7,9 @@ import { matchContainerName } from '../../utils/symbol_utils'; describe('matchSymbolName', () => { + it('should match symbol whose name is exactly the container name', () => { + expect(matchContainerName('Session', 'Session')).toBe(true); + }); it('should match symbol that has type annotation', () => { expect(matchContainerName('Session', 'Session')).toBe(true); }); diff --git a/x-pack/plugins/code/public/utils/symbol_utils.ts b/x-pack/plugins/code/public/utils/symbol_utils.ts index dd30a0e8e4703..fe659c703b19f 100644 --- a/x-pack/plugins/code/public/utils/symbol_utils.ts +++ b/x-pack/plugins/code/public/utils/symbol_utils.ts @@ -5,4 +5,4 @@ */ export const matchContainerName = (containerName: string, symbolName: string) => - new RegExp(`^${containerName}[[<(].*[>)]]?$`).test(symbolName); + new RegExp(`^${containerName}([<(].*[>)])?$`).test(symbolName); diff --git a/x-pack/plugins/code/server/indexer/es_exception.ts b/x-pack/plugins/code/server/indexer/es_exception.ts new file mode 100644 index 0000000000000..e0c3fdf5015cf --- /dev/null +++ b/x-pack/plugins/code/server/indexer/es_exception.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum EsException { + INDEX_NOT_FOUND_EXCEPTION = 'index_not_found_exception', + RESOURCE_ALREADY_EXISTS_EXCEPTION = 'resource_already_exists_exception', +} diff --git a/x-pack/plugins/code/server/indexer/index_migrator.ts b/x-pack/plugins/code/server/indexer/index_migrator.ts index e30e5475a47f8..a3a8c4d5cbd72 100644 --- a/x-pack/plugins/code/server/indexer/index_migrator.ts +++ b/x-pack/plugins/code/server/indexer/index_migrator.ts @@ -15,6 +15,7 @@ import { Repository } from '../../model'; import { EsClient } from '../lib/esqueue'; import { Logger } from '../log'; import { RepositoryObjectClient } from '../search'; +import { EsException } from './es_exception'; import pkg from './schema/version.json'; export class IndexMigrator { @@ -57,9 +58,18 @@ export class IndexMigrator { body, }); } catch (error) { - this.log.error(`Create new index ${newIndexName} for index migration error.`); - this.log.error(error); - throw error; + if ( + error.body && + error.body.error && + error.body.error.type === EsException.RESOURCE_ALREADY_EXISTS_EXCEPTION + ) { + this.log.debug(`The target verion index already exists. Skip index migration`); + return; + } else { + this.log.error(`Create new index ${newIndexName} for index migration error.`); + this.log.error(error); + throw error; + } } try { diff --git a/x-pack/plugins/code/server/indexer/index_version_controller.ts b/x-pack/plugins/code/server/indexer/index_version_controller.ts index aa087337bb6ac..e753b9f97b1c4 100644 --- a/x-pack/plugins/code/server/indexer/index_version_controller.ts +++ b/x-pack/plugins/code/server/indexer/index_version_controller.ts @@ -9,11 +9,13 @@ import _ from 'lodash'; import { IndexMigrator } from '.'; import { EsClient } from '../lib/esqueue'; import { Logger } from '../log'; +import { EsException } from './es_exception'; import { IndexCreationRequest } from './index_creation_request'; import pkg from './schema/version.json'; export class IndexVersionController { private version: number; + private DEFAULT_VERSION = 0; constructor(protected readonly client: EsClient, private readonly log: Logger) { this.version = Number(pkg.codeIndexVersion); @@ -21,17 +23,30 @@ export class IndexVersionController { public async tryUpgrade(request: IndexCreationRequest) { this.log.debug(`Try upgrade index mapping/settings for index ${request.index}.`); - const esIndexVersion = await this.getIndexVersionFromES(request.index); - const needUpgrade = this.needUpgrade(esIndexVersion); - if (needUpgrade) { - const migrator = new IndexMigrator(this.client, this.log); - const oldIndexName = `${request.index}-${esIndexVersion}`; - this.log.warn( - `Migrate index mapping/settings from version ${esIndexVersion} for ${request.index}` - ); - return migrator.migrateIndex(oldIndexName, request); - } else { - this.log.debug(`Index version is update-to-date for ${request.index}`); + try { + const esIndexVersion = await this.getIndexVersionFromES(request.index); + const needUpgrade = this.needUpgrade(esIndexVersion); + if (needUpgrade) { + const migrator = new IndexMigrator(this.client, this.log); + const oldIndexName = `${request.index}-${esIndexVersion}`; + this.log.info( + `Migrate index mapping/settings from version ${esIndexVersion} for ${request.index}` + ); + return migrator.migrateIndex(oldIndexName, request); + } else { + this.log.debug(`Index version is update-to-date for ${request.index}`); + } + } catch (error) { + if ( + error.body && + error.body.error && + error.body.error.type === EsException.INDEX_NOT_FOUND_EXCEPTION + ) { + this.log.info(`Skip upgrade index ${request.index} because original index does not exist.`); + } else { + this.log.error(`Try upgrade index error for ${request.index}.`); + this.log.error(error); + } } } @@ -44,20 +59,14 @@ export class IndexVersionController { } private async getIndexVersionFromES(indexName: string): Promise { - try { - const res = await this.client.indices.getMapping({ - index: indexName, - }); - const esIndexName = Object.keys(res)[0]; - const version = _.get(res, [esIndexName, 'mappings', '_meta', 'version'], 0); - if (version === 0) { - this.log.error(`Can't find index version for ${indexName}.`); - } - return version; - } catch (error) { - this.log.error(`Get index version error for ${indexName}.`); - this.log.error(error); - return 0; + const res = await this.client.indices.getMapping({ + index: indexName, + }); + const esIndexName = Object.keys(res)[0]; + const version = _.get(res, [esIndexName, 'mappings', '_meta', 'version'], this.DEFAULT_VERSION); + if (version === this.DEFAULT_VERSION) { + this.log.warn(`Can't find index version field in _meta for ${indexName}.`); } + return version; } } diff --git a/x-pack/plugins/code/server/indexer/lsp_incremental_indexer.ts b/x-pack/plugins/code/server/indexer/lsp_incremental_indexer.ts index c0a6c1dc5a494..0ac5af08477a4 100644 --- a/x-pack/plugins/code/server/indexer/lsp_incremental_indexer.ts +++ b/x-pack/plugins/code/server/indexer/lsp_incremental_indexer.ts @@ -155,9 +155,14 @@ export class LspIncrementalIndexer extends LspIndexer { this.diff = await this.gitOps.getDiff(this.repoUri, this.originRevision, this.revision); return this.diff.files.length; } catch (error) { - this.log.error(`Get lsp incremental index requests count error.`); - this.log.error(error); - throw error; + if (this.isCancelled()) { + this.log.debug(`Incremental indexer got cancelled. Skip get index count error.`); + return 1; + } else { + this.log.error(`Get lsp incremental index requests count error.`); + this.log.error(error); + throw error; + } } } diff --git a/x-pack/plugins/code/server/indexer/lsp_indexer.ts b/x-pack/plugins/code/server/indexer/lsp_indexer.ts index fcb21cb19c313..a293c896e6c0a 100644 --- a/x-pack/plugins/code/server/indexer/lsp_indexer.ts +++ b/x-pack/plugins/code/server/indexer/lsp_indexer.ts @@ -135,9 +135,14 @@ export class LspIndexer extends AbstractIndexer { try { return await this.gitOps.countRepoFiles(this.repoUri, 'head'); } catch (error) { - this.log.error(`Get lsp index requests count error.`); - this.log.error(error); - throw error; + if (this.isCancelled()) { + this.log.debug(`Indexer got cancelled. Skip get index count error.`); + return 1; + } else { + this.log.error(`Get lsp index requests count error.`); + this.log.error(error); + throw error; + } } } diff --git a/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts b/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts deleted file mode 100644 index cc540e5eaa127..0000000000000 --- a/x-pack/plugins/code/server/lsp/abstract_launcher.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// eslint-disable-next-line max-classes-per-file -import { fork, ChildProcess } from 'child_process'; -import path from 'path'; -import fs from 'fs'; - -import { ServerOptions } from '../server_options'; -import { createTestServerOption } from '../test_utils'; -import { AbstractLauncher, ServerStartFailed } from './abstract_launcher'; -import { RequestExpander } from './request_expander'; -import { LanguageServerProxy } from './proxy'; -import { ConsoleLoggerFactory } from '../utils/console_logger_factory'; -import { Logger } from '../log'; -import getPort from 'get-port'; - -jest.setTimeout(40000); - -// @ts-ignore -const options: ServerOptions = createTestServerOption(); - -// a mock function being called when then forked sub process status changes -// @ts-ignore -const mockMonitor = jest.fn(); - -class MockLauncher extends AbstractLauncher { - public childProcess?: ChildProcess; - - constructor(name: string, targetHost: string, opt: ServerOptions) { - super(name, targetHost, opt, new ConsoleLoggerFactory()); - } - - protected maxRespawn = 3; - - createExpander( - proxy: LanguageServerProxy, - builtinWorkspace: boolean, - maxWorkspace: number - ): RequestExpander { - return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options); - } - - async getPort() { - return await getPort(); - } - - async spawnProcess(installationPath: string, port: number, log: Logger): Promise { - const childProcess = fork(path.join(__dirname, 'mock_lang_server.js')); - this.childProcess = childProcess; - childProcess.on('message', msg => { - // eslint-disable-next-line no-console - console.log(msg); - mockMonitor(msg); - }); - childProcess.send(`port ${port}`); - childProcess.send(`host ${this.targetHost}`); - childProcess.send('listen'); - return childProcess; - } - - protected killProcess(child: ChildProcess): Promise { - // don't kill the process so fast, otherwise no normal exit can happen - return new Promise(resolve => { - setTimeout(async () => { - const killed = await super.killProcess(child); - resolve(killed); - }, 100); - }); - } -} - -class PassiveMockLauncher extends MockLauncher { - constructor( - name: string, - targetHost: string, - opt: ServerOptions, - private dieBeforeStart: number = 0 - ) { - super(name, targetHost, opt); - } - - startConnect(proxy: LanguageServerProxy) { - proxy.awaitServerConnection().catch(this.log.debug); - } - - async getPort() { - return 19998; - } - - async spawnProcess(installationPath: string, port: number, log: Logger): Promise { - this.childProcess = fork(path.join(__dirname, 'mock_lang_server.js')); - this.childProcess.on('message', msg => { - // eslint-disable-next-line no-console - console.log(msg); - mockMonitor(msg); - }); - this.childProcess.send(`port ${port}`); - this.childProcess.send(`host ${this.targetHost}`); - if (this.dieBeforeStart > 0) { - this.childProcess!.send('quit'); - this.dieBeforeStart -= 1; - } else { - this.childProcess!.send('connect'); - } - return this.childProcess!; - } -} - -beforeAll(async () => { - if (!fs.existsSync(options.workspacePath)) { - fs.mkdirSync(options.workspacePath, { recursive: true }); - fs.mkdirSync(options.jdtWorkspacePath, { recursive: true }); - } -}); - -beforeEach(() => { - mockMonitor.mockClear(); -}); - -function delay(millis: number) { - return new Promise(resolve => { - setTimeout(() => resolve(), millis); - }); -} - -async function retryUtil(millis: number, testFn: () => void, interval = 1000) { - try { - testFn(); - } catch (e) { - if (millis >= 0) { - await delay(interval); - await retryUtil(millis - interval, testFn); - } else { - throw e; - } - } -} - -// FLAKY: https://github.com/elastic/kibana/issues/38791 -test.skip('launcher can start and end a process', async () => { - const launcher = new MockLauncher('mock', 'localhost', options); - const proxy = await launcher.launch(false, 1, ''); - await retryUtil(1000, () => { - expect(mockMonitor.mock.calls[0][0]).toBe('process started'); - expect(mockMonitor.mock.calls[1][0]).toBe('start listening'); - expect(mockMonitor.mock.calls[2][0]).toBe('socket connected'); - }); - await proxy.exit(); - await retryUtil(1000, () => { - expect(mockMonitor.mock.calls[3][0]).toMatchObject({ method: 'shutdown' }); - expect(mockMonitor.mock.calls[4][0]).toMatchObject({ method: 'exit' }); - expect(mockMonitor.mock.calls[5][0]).toBe('exit process with code 0'); - }); -}); - -test('launcher can force kill the process if langServer can not exit', async () => { - const launcher = new MockLauncher('mock', 'localhost', options); - const proxy = await launcher.launch(false, 1, ''); - await delay(100); - // set mock lang server to noExist mode - launcher.childProcess!.send('noExit'); - mockMonitor.mockClear(); - await proxy.exit(); - await retryUtil(30000, () => { - expect(mockMonitor.mock.calls[0][0]).toMatchObject({ method: 'shutdown' }); - expect(mockMonitor.mock.calls[1][0]).toMatchObject({ method: 'exit' }); - expect(mockMonitor.mock.calls[2][0]).toBe('noExit'); - expect(launcher.childProcess!.killed).toBe(true); - }); -}); - -test('launcher can reconnect if process died', async () => { - const launcher = new MockLauncher('mock', 'localhost', options); - const proxy = await launcher.launch(false, 1, ''); - await delay(1000); - mockMonitor.mockClear(); - // let the process quit - launcher.childProcess!.send('quit'); - await retryUtil(30000, () => { - // launcher should respawn a new process and connect - expect(mockMonitor.mock.calls[0][0]).toBe('process started'); - expect(mockMonitor.mock.calls[1][0]).toBe('start listening'); - expect(mockMonitor.mock.calls[2][0]).toBe('socket connected'); - }); - await proxy.exit(); - await delay(1000); -}); - -// FLAKY: https://github.com/elastic/kibana/issues/38849 -test.skip('passive launcher can start and end a process', async () => { - const launcher = new PassiveMockLauncher('mock', 'localhost', options); - const proxy = await launcher.launch(false, 1, ''); - await retryUtil(30000, () => { - expect(mockMonitor.mock.calls[0][0]).toBe('process started'); - expect(mockMonitor.mock.calls[1][0]).toBe('start connecting'); - expect(mockMonitor.mock.calls[2][0]).toBe('socket connected'); - }); - await proxy.exit(); - await retryUtil(30000, () => { - expect(mockMonitor.mock.calls[3][0]).toMatchObject({ method: 'shutdown' }); - expect(mockMonitor.mock.calls[4][0]).toMatchObject({ method: 'exit' }); - expect(mockMonitor.mock.calls[5][0]).toBe('exit process with code 0'); - }); -}); - -test('passive launcher should restart a process if a process died before connected', async () => { - const launcher = new PassiveMockLauncher('mock', 'localhost', options, 1); - const proxy = await launcher.launch(false, 1, ''); - await delay(100); - await retryUtil(30000, () => { - expect(mockMonitor.mock.calls[0][0]).toBe('process started'); - expect(mockMonitor.mock.calls[1][0]).toBe('process started'); - expect(mockMonitor.mock.calls[2][0]).toBe('start connecting'); - expect(mockMonitor.mock.calls[3][0]).toBe('socket connected'); - }); - await proxy.exit(); - await delay(1000); -}); - -test('launcher should mark proxy unusable after restart 2 times', async () => { - const launcher = new PassiveMockLauncher('mock', 'localhost', options, 3); - try { - await launcher.launch(false, 1, ''); - } catch (e) { - await retryUtil(30000, () => { - expect(mockMonitor.mock.calls[0][0]).toBe('process started'); - // restart 2 times - expect(mockMonitor.mock.calls[1][0]).toBe('process started'); - expect(mockMonitor.mock.calls[2][0]).toBe('process started'); - }); - expect(e).toEqual(ServerStartFailed); - await delay(1000); - } -}); diff --git a/x-pack/plugins/code/server/lsp/abstract_launcher.ts b/x-pack/plugins/code/server/lsp/abstract_launcher.ts index 21a6d5ba83b9a..9f29d32accadb 100644 --- a/x-pack/plugins/code/server/lsp/abstract_launcher.ts +++ b/x-pack/plugins/code/server/lsp/abstract_launcher.ts @@ -87,7 +87,6 @@ export abstract class AbstractLauncher implements ILanguageServerLauncher { this.killProcess(p); } }); - proxy.listen(); this.startConnect(proxy); await new Promise((resolve, reject) => { proxy.onConnected(() => { diff --git a/x-pack/plugins/code/server/lsp/go_launcher.ts b/x-pack/plugins/code/server/lsp/go_launcher.ts index 5d5c127c6082b..a6cffe1baa35b 100644 --- a/x-pack/plugins/code/server/lsp/go_launcher.ts +++ b/x-pack/plugins/code/server/lsp/go_launcher.ts @@ -4,43 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ChildProcess } from 'child_process'; +import getPort from 'get-port'; +import { Logger, MarkupKind } from 'vscode-languageserver-protocol'; import { ServerOptions } from '../server_options'; import { LoggerFactory } from '../utils/log_factory'; -import { ILanguageServerLauncher } from './language_server_launcher'; +import { AbstractLauncher } from './abstract_launcher'; import { LanguageServerProxy } from './proxy'; -import { RequestExpander } from './request_expander'; +import { InitializeOptions, RequestExpander } from './request_expander'; -export class GoLauncher implements ILanguageServerLauncher { - private isRunning: boolean = false; - constructor( +const GO_LANG_DETACH_PORT = 2091; + +export class GoServerLauncher extends AbstractLauncher { + public constructor( readonly targetHost: string, readonly options: ServerOptions, readonly loggerFactory: LoggerFactory - ) {} - public get running(): boolean { - return this.isRunning; + ) { + super('go', targetHost, options, loggerFactory); } - public async launch(builtinWorkspace: boolean, maxWorkspace: number, installationPath: string) { - const port = 2091; - - const log = this.loggerFactory.getLogger(['code', `go@${this.targetHost}:${port}`]); - const proxy = new LanguageServerProxy(port, this.targetHost, log, this.options.lsp); - - log.info('Detach mode, expected langserver launch externally'); - proxy.onConnected(() => { - this.isRunning = true; - }); - proxy.onDisconnected(() => { - this.isRunning = false; - if (!proxy.isClosed) { - log.warn('language server disconnected, reconnecting'); - setTimeout(() => proxy.connect(), 1000); - } - }); + async getPort() { + if (!this.options.lsp.detach) { + return await getPort(); + } + return GO_LANG_DETACH_PORT; + } - proxy.listen(); - await proxy.connect(); - return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options); + createExpander( + proxy: LanguageServerProxy, + builtinWorkspace: boolean, + maxWorkspace: number + ): RequestExpander { + return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options, { + clientCapabilities: { + textDocument: { + hover: { + contentFormat: [MarkupKind.Markdown, MarkupKind.PlainText], + }, + }, + }, + } as InitializeOptions); + } + // TODO(henrywong): Once go langugage server ready to release, we should support this mode. + async spawnProcess(installationPath: string, port: number, log: Logger): Promise { + throw new Error('Go language server currently only support detach mode'); } } diff --git a/x-pack/plugins/code/server/lsp/http_message_reader.ts b/x-pack/plugins/code/server/lsp/http_message_reader.ts deleted file mode 100644 index 9c7150036b325..0000000000000 --- a/x-pack/plugins/code/server/lsp/http_message_reader.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - AbstractMessageReader, - DataCallback, - MessageReader, -} from 'vscode-jsonrpc/lib/messageReader'; - -import { HttpRequestEmitter } from './http_request_emitter'; - -export class HttpMessageReader extends AbstractMessageReader implements MessageReader { - private httpEmitter: HttpRequestEmitter; - - public constructor(httpEmitter: HttpRequestEmitter) { - super(); - httpEmitter.on('error', (error: any) => this.fireError(error)); - httpEmitter.on('close', () => this.fireClose()); - this.httpEmitter = httpEmitter; - } - - public listen(callback: DataCallback): void { - this.httpEmitter.on('message', callback); - } -} diff --git a/x-pack/plugins/code/server/lsp/http_message_writer.ts b/x-pack/plugins/code/server/lsp/http_message_writer.ts deleted file mode 100644 index 761f470292e33..0000000000000 --- a/x-pack/plugins/code/server/lsp/http_message_writer.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Message, ResponseMessage } from 'vscode-jsonrpc/lib/messages'; -import { AbstractMessageWriter, MessageWriter } from 'vscode-jsonrpc/lib/messageWriter'; -import { Logger } from '../log'; - -import { RepliesMap } from './replies_map'; - -export class HttpMessageWriter extends AbstractMessageWriter implements MessageWriter { - private replies: RepliesMap; - private logger: Logger | undefined; - - constructor(replies: RepliesMap, logger: Logger | undefined) { - super(); - this.replies = replies; - this.logger = logger; - } - - public write(msg: Message): void { - const response = msg as ResponseMessage; - if (response.id != null) { - // this is a response - const id = response.id as number; - const reply = this.replies.get(id); - if (reply) { - this.replies.delete(id); - const [resolve, reject] = reply; - if (response.error) { - reject(response.error); - } else { - resolve(response); - } - } else { - if (this.logger) { - this.logger.error('missing reply functions for ' + id); - } - } - } else { - if (this.logger) { - this.logger.info(`ignored message ${JSON.stringify(msg)} because of no id`); - } - } - } -} diff --git a/x-pack/plugins/code/server/lsp/java_launcher.ts b/x-pack/plugins/code/server/lsp/java_launcher.ts index 877d5c374b6b4..373877e0b67ca 100644 --- a/x-pack/plugins/code/server/lsp/java_launcher.ts +++ b/x-pack/plugins/code/server/lsp/java_launcher.ts @@ -13,9 +13,9 @@ import path from 'path'; import { Logger } from '../log'; import { ServerOptions } from '../server_options'; import { LoggerFactory } from '../utils/log_factory'; -import { LanguageServerProxy } from './proxy'; -import { RequestExpander } from './request_expander'; import { AbstractLauncher } from './abstract_launcher'; +import { LanguageServerProxy } from './proxy'; +import { InitializeOptions, RequestExpander } from './request_expander'; const JAVA_LANG_DETACH_PORT = 2090; @@ -31,12 +31,14 @@ export class JavaLauncher extends AbstractLauncher { createExpander(proxy: LanguageServerProxy, builtinWorkspace: boolean, maxWorkspace: number) { return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options, { - settings: { - 'java.import.gradle.enabled': this.options.security.enableGradleImport, - 'java.import.maven.enabled': this.options.security.enableMavenImport, - 'java.autobuild.enabled': false, + initialOptions: { + settings: { + 'java.import.gradle.enabled': this.options.security.enableGradleImport, + 'java.import.maven.enabled': this.options.security.enableMavenImport, + 'java.autobuild.enabled': false, + }, }, - }); + } as InitializeOptions); } startConnect(proxy: LanguageServerProxy) { diff --git a/x-pack/plugins/code/server/lsp/language_servers.ts b/x-pack/plugins/code/server/lsp/language_servers.ts index 1ef55eba0d9ee..2083dca22521d 100644 --- a/x-pack/plugins/code/server/lsp/language_servers.ts +++ b/x-pack/plugins/code/server/lsp/language_servers.ts @@ -6,11 +6,11 @@ import { InstallationType } from '../../common/installation'; import { LanguageServer } from '../../common/language_server'; -import { GoLauncher } from './go_launcher'; +import { CtagsLauncher } from './ctags_launcher'; +import { GoServerLauncher } from './go_launcher'; import { JavaLauncher } from './java_launcher'; import { LauncherConstructor } from './language_server_launcher'; import { TypescriptServerLauncher } from './ts_launcher'; -import { CtagsLauncher } from './ctags_launcher'; export interface LanguageServerDefinition extends LanguageServer { builtinWorkspaceFolders: boolean; @@ -46,7 +46,7 @@ export const GO: LanguageServerDefinition = { name: 'Go', builtinWorkspaceFolders: true, languages: ['go'], - launcher: GoLauncher, + launcher: GoServerLauncher, installationType: InstallationType.Plugin, installationPluginName: 'goLanguageServer', }; diff --git a/x-pack/plugins/code/server/lsp/proxy.ts b/x-pack/plugins/code/server/lsp/proxy.ts index 950f38c53ca52..c1ecaadd5b939 100644 --- a/x-pack/plugins/code/server/lsp/proxy.ts +++ b/x-pack/plugins/code/server/lsp/proxy.ts @@ -12,8 +12,7 @@ import { SocketMessageReader, SocketMessageWriter, } from 'vscode-jsonrpc'; -import { RequestMessage, ResponseMessage } from 'vscode-jsonrpc/lib/messages'; - +import { ResponseMessage } from 'vscode-jsonrpc/lib/messages'; import { ClientCapabilities, ExitNotification, @@ -23,16 +22,12 @@ import { MessageType, WorkspaceFolder, } from 'vscode-languageserver-protocol/lib/main'; -import { createConnection, IConnection } from 'vscode-languageserver/lib/main'; - +import { RequestCancelled } from '../../common/lsp_error_codes'; import { LspRequest } from '../../model'; import { Logger } from '../log'; import { LspOptions } from '../server_options'; -import { HttpMessageReader } from './http_message_reader'; -import { HttpMessageWriter } from './http_message_writer'; -import { HttpRequestEmitter } from './http_request_emitter'; -import { createRepliesMap } from './replies_map'; import { Cancelable } from '../utils/cancelable'; +import { InitializeOptions } from './request_expander'; export interface ILanguageServerHandler { lastAccess?: number; @@ -49,12 +44,8 @@ export class LanguageServerProxy implements ILanguageServerHandler { public initialized: boolean = false; private socket: any; - private conn: IConnection; private clientConnection: MessageConnection | null = null; private closed: boolean = false; - private sequenceNumber = 0; - private httpEmitter = new HttpRequestEmitter(); - private replies = createRepliesMap(); private readonly targetHost: string; private targetPort: number; private readonly logger: Logger; @@ -67,51 +58,49 @@ export class LanguageServerProxy implements ILanguageServerHandler { this.targetPort = targetPort; this.logger = logger; this.lspOptions = lspOptions; - this.conn = createConnection( - new HttpMessageReader(this.httpEmitter), - new HttpMessageWriter(this.replies, logger) - ); - } - public handleRequest(request: LspRequest): Promise { - return this.receiveRequest(request.method, request.params, request.isNotification); } - public receiveRequest(method: string, params: any, isNotification: boolean = false) { - if (this.error) { - return Promise.reject(this.error); - } - const message: RequestMessage = { - jsonrpc: '2.0', - id: this.sequenceNumber++, - method, - params, + public async handleRequest(request: LspRequest): Promise { + const response: ResponseMessage = { + jsonrpc: '', + id: null, + error: { code: RequestCancelled, message: 'Server closed' }, }; - return new Promise((resolve, reject) => { - if (this.lspOptions.verbose) { - this.logger.info(`emit message ${JSON.stringify(message)}`); - } else { - this.logger.debug(`emit message ${JSON.stringify(message)}`); - } - if (isNotification) { - // for language server as jdt, notification won't have a response message. - this.httpEmitter.emit('message', message); - resolve(); + if (this.closed) { + response.error = { code: RequestCancelled, message: 'Server closed' }; + } else { + const conn = await this.connect(); + const params = Array.isArray(request.params) ? request.params : [request.params]; + if (!request.isNotification) { + try { + response.result = await conn.sendRequest(request.method, ...params); + } catch (error) { + response.error = error; + } } else { - this.replies.set(message.id as number, [resolve, reject]); - this.httpEmitter.emit('message', message); + conn.sendNotification(request.method, ...params); } - }); + } + return response; } + public async initialize( clientCapabilities: ClientCapabilities, workspaceFolders: [WorkspaceFolder], - initOptions?: object + initOptions?: InitializeOptions ): Promise { if (this.error) { throw this.error; } const clientConn = await this.connect(); const rootUri = workspaceFolders[0].uri; + if ( + initOptions && + initOptions.clientCapabilities && + Object.keys(clientCapabilities).length === 0 + ) { + clientCapabilities = initOptions.clientCapabilities; + } const params = { processId: null, workspaceFolders, @@ -122,7 +111,9 @@ export class LanguageServerProxy implements ILanguageServerHandler { return await clientConn .sendRequest( 'initialize', - initOptions ? { ...params, initializationOptions: initOptions } : params + initOptions && initOptions.initialOptions + ? { ...params, initializationOptions: initOptions.initialOptions } + : params ) .then(r => { this.logger.info(`initialized at ${rootUri}`); @@ -135,27 +126,6 @@ export class LanguageServerProxy implements ILanguageServerHandler { }); } - public listen() { - this.conn.onRequest((method: string, ...params) => { - if (this.lspOptions.verbose) { - this.logger.info('received request method: ' + method); - } else { - this.logger.debug('received request method: ' + method); - } - - return this.connect().then(clientConn => { - if (this.lspOptions.verbose) { - this.logger.info(`proxy method:${method} to Language Server `); - } else { - this.logger.debug(`proxy method:${method} to Language Server `); - } - - return clientConn.sendRequest(method, ...params); - }); - }); - this.conn.listen(); - } - public async shutdown() { const clientConn = await this.connect(); this.logger.info(`sending shutdown request`); @@ -170,9 +140,7 @@ export class LanguageServerProxy implements ILanguageServerHandler { if (this.clientConnection) { this.logger.info('sending `shutdown` request to language server.'); const clientConn = this.clientConnection; - clientConn.sendRequest('shutdown').then(() => { - this.conn.dispose(); - }); + clientConn.sendRequest('shutdown'); this.logger.info('sending `exit` notification to language server.'); // @ts-ignore clientConn.sendNotification(ExitNotification.type); @@ -237,7 +205,6 @@ export class LanguageServerProxy implements ILanguageServerHandler { if (this.clientConnection) { return Promise.resolve(this.clientConnection); } - this.closed = false; if (!this.connectingPromise) { this.connectingPromise = new Cancelable(resolve => { this.socket = new net.Socket(); diff --git a/x-pack/plugins/code/server/lsp/request_expander.test.ts b/x-pack/plugins/code/server/lsp/request_expander.test.ts index e0bb46bccc0f7..eca78805039b6 100644 --- a/x-pack/plugins/code/server/lsp/request_expander.test.ts +++ b/x-pack/plugins/code/server/lsp/request_expander.test.ts @@ -12,7 +12,7 @@ import { pathToFileURL } from 'url'; import { ServerOptions } from '../server_options'; import { LanguageServerProxy } from './proxy'; -import { InitializingError, RequestExpander } from './request_expander'; +import { InitializingError, RequestExpander, WorkspaceUnloadedError } from './request_expander'; // @ts-ignore const options: ServerOptions = { @@ -31,19 +31,41 @@ afterEach(() => { }); }); -test('requests should be sequential', async () => { - const clock = sinon.useFakeTimers(); +function createMockProxy(initDelay: number = 0, requestDelay: number = 0) { // @ts-ignore const proxyStub = sinon.createStubInstance(LanguageServerProxy, { handleRequest: sinon.stub().callsFake(() => { - const start = Date.now(); - return new Promise(resolve => { - setTimeout(() => { - resolve({ result: { start, end: Date.now() } }); - }, 100); - }); + if (requestDelay > 0) { + const start = Date.now(); + return new Promise(resolve => { + setTimeout(() => { + resolve({ result: { start, end: Date.now() } }); + }, requestDelay); + }); + } else { + return sinon.stub().resolvesArg(0); + } }), + initialize: sinon.stub().callsFake( + () => + new Promise(resolve => { + proxyStub.initialized = true; + if (initDelay > 0) { + setTimeout(() => { + resolve(); + }, initDelay); + } else { + resolve(); + } + }) + ), }); + return proxyStub; +} + +test('requests should be sequential', async () => { + const clock = sinon.useFakeTimers(); + const proxyStub = createMockProxy(0, 100); const expander = new RequestExpander(proxyStub, false, 1, options); const request1 = { method: 'request1', @@ -66,20 +88,7 @@ test('requests should be sequential', async () => { test('requests should throw error after lsp init timeout', async () => { const clock = sinon.useFakeTimers(); - // @ts-ignore - const proxyStub = sinon.createStubInstance(LanguageServerProxy, { - handleRequest: sinon.stub().callsFake(() => { - Promise.resolve('ok'); - }), - initialize: sinon.stub().callsFake( - () => - new Promise(resolve => { - setTimeout(() => { - resolve(); - }, 300); - }) - ), - }); + const proxyStub = createMockProxy(300); const expander = new RequestExpander(proxyStub, false, 1, options); const request1 = { method: 'request1', @@ -105,14 +114,7 @@ test('requests should throw error after lsp init timeout', async () => { }); test('be able to open multiple workspace', async () => { - // @ts-ignore - const proxyStub = sinon.createStubInstance(LanguageServerProxy, { - initialize: sinon.stub().callsFake(() => { - proxyStub.initialized = true; - return Promise.resolve(); - }), - handleRequest: sinon.stub().resolvesArg(0), - }); + const proxyStub = createMockProxy(); const expander = new RequestExpander(proxyStub, true, 2, options); const request1 = { method: 'request1', @@ -158,14 +160,7 @@ test('be able to open multiple workspace', async () => { }); test('be able to swap workspace', async () => { - // @ts-ignore - const proxyStub = sinon.createStubInstance(LanguageServerProxy, { - initialize: sinon.stub().callsFake(() => { - proxyStub.initialized = true; - return Promise.resolve(); - }), - handleRequest: sinon.stub().resolvesArg(0), - }); + const proxyStub = createMockProxy(); const expander = new RequestExpander(proxyStub, true, 1, options); const request1 = { method: 'request1', @@ -205,3 +200,27 @@ test('be able to swap workspace', async () => { }) ).toBeTruthy(); }); + +test('requests should be cancelled if workspace is unloaded', async () => { + // @ts-ignore + const clock = sinon.useFakeTimers(); + const proxyStub = createMockProxy(300); + const expander = new RequestExpander(proxyStub, true, 1, options); + const workspace1 = '/tmp/test/workspace/1'; + const request = { + method: 'request1', + params: [], + workspacePath: workspace1, + timeoutForInitializeMs: 500, + }; + mkdirp.sync(workspace1); + const promise1 = expander.handleRequest(request); + clock.tick(100); + const promise2 = expander.handleRequest(request); + await expander.unloadWorkspace(workspace1); + clock.tick(400); + process.nextTick(() => clock.runAll()); + await expect(promise1).rejects.toEqual(WorkspaceUnloadedError); + await expect(promise2).rejects.toEqual(WorkspaceUnloadedError); + clock.restore(); +}); diff --git a/x-pack/plugins/code/server/lsp/request_expander.ts b/x-pack/plugins/code/server/lsp/request_expander.ts index c8fa1ad642d54..2a9d254420460 100644 --- a/x-pack/plugins/code/server/lsp/request_expander.ts +++ b/x-pack/plugins/code/server/lsp/request_expander.ts @@ -7,13 +7,17 @@ import fs from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; - import { ResponseError, ResponseMessage } from 'vscode-jsonrpc/lib/messages'; -import { DidChangeWorkspaceFoldersParams, InitializeResult } from 'vscode-languageserver-protocol'; - -import { ServerNotInitialized } from '../../common/lsp_error_codes'; +import { + ClientCapabilities, + DidChangeWorkspaceFoldersParams, + InitializeResult, +} from 'vscode-languageserver-protocol'; +import { RequestCancelled, ServerNotInitialized } from '../../common/lsp_error_codes'; import { LspRequest } from '../../model'; +import { Logger } from '../log'; import { ServerOptions } from '../server_options'; +import { Cancelable } from '../utils/cancelable'; import { promiseTimeout } from '../utils/timeout'; import { ILanguageServerHandler, LanguageServerProxy } from './proxy'; @@ -33,10 +37,16 @@ enum WorkspaceStatus { interface Workspace { lastAccess: number; status: WorkspaceStatus; - initPromise?: Promise; + initPromise?: Cancelable; +} + +export interface InitializeOptions { + clientCapabilities?: ClientCapabilities; + initialOptions?: object; } export const InitializingError = new ResponseError(ServerNotInitialized, 'Server is initializing'); +export const WorkspaceUnloadedError = new ResponseError(RequestCancelled, 'Workspace unloaded'); export class RequestExpander implements ILanguageServerHandler { public lastAccess: number = 0; @@ -53,7 +63,8 @@ export class RequestExpander implements ILanguageServerHandler { readonly builtinWorkspace: boolean, readonly maxWorkspace: number, readonly serverOptions: ServerOptions, - readonly initialOptions?: object + readonly initialOptions?: InitializeOptions, + readonly log?: Logger ) { this.proxy = proxy; this.handle = this.handle.bind(this); @@ -75,6 +86,8 @@ export class RequestExpander implements ILanguageServerHandler { reject, startTime: Date.now(), }); + if (this.log) + this.log.debug(`queued a ${request.method} job for workspace ${request.workspacePath}`); if (!this.running) { this.running = true; this.handleNext(); @@ -88,9 +101,13 @@ export class RequestExpander implements ILanguageServerHandler { } public async unloadWorkspace(workspacePath: string) { + if (this.log) this.log.debug('unload workspace ' + workspacePath); if (this.hasWorkspacePath(workspacePath)) { + const ws = this.getWorkspace(workspacePath); + if (ws.initPromise) { + ws.initPromise.cancel(WorkspaceUnloadedError); + } if (this.builtinWorkspace) { - this.removeWorkspace(workspacePath); const params: DidChangeWorkspaceFoldersParams = { event: { removed: [ @@ -111,6 +128,18 @@ export class RequestExpander implements ILanguageServerHandler { await this.exit(); } } + this.removeWorkspace(workspacePath); + const newJobQueue: Job[] = []; + this.jobQueue.forEach(job => { + if (job.request.workspacePath === workspacePath) { + job.reject(WorkspaceUnloadedError); + if (this.log) + this.log.debug(`canceled a ${job.request.method} job because of unload workspace`); + } else { + newJobQueue.push(job); + } + }); + this.jobQueue = newJobQueue; } public async initialize(workspacePath: string): Promise { @@ -191,7 +220,7 @@ export class RequestExpander implements ILanguageServerHandler { if (request.workspacePath) { const ws = this.getWorkspace(request.workspacePath); if (ws.status === WorkspaceStatus.Uninitialized) { - ws.initPromise = this.initialize(request.workspacePath); + ws.initPromise = Cancelable.fromPromise(this.initialize(request.workspacePath)); } // Uninitialized or initializing if (ws.status !== WorkspaceStatus.Initialized) { @@ -200,7 +229,7 @@ export class RequestExpander implements ILanguageServerHandler { if (timeout > 0 && ws.initPromise) { try { const elapsed = Date.now() - startTime; - await promiseTimeout(timeout - elapsed, ws.initPromise); + await promiseTimeout(timeout - elapsed, ws.initPromise.promise); } catch (e) { if (e.isTimeout) { throw InitializingError; @@ -208,7 +237,7 @@ export class RequestExpander implements ILanguageServerHandler { throw e; } } else if (ws.initPromise) { - await ws.initPromise; + await ws.initPromise.promise; } else { throw InitializingError; } diff --git a/x-pack/plugins/code/server/lsp/ts_launcher.ts b/x-pack/plugins/code/server/lsp/ts_launcher.ts index 397ec123ae08f..521277fb7e346 100644 --- a/x-pack/plugins/code/server/lsp/ts_launcher.ts +++ b/x-pack/plugins/code/server/lsp/ts_launcher.ts @@ -10,9 +10,9 @@ import { resolve } from 'path'; import { Logger } from '../log'; import { ServerOptions } from '../server_options'; import { LoggerFactory } from '../utils/log_factory'; -import { LanguageServerProxy } from './proxy'; -import { RequestExpander } from './request_expander'; import { AbstractLauncher } from './abstract_launcher'; +import { LanguageServerProxy } from './proxy'; +import { InitializeOptions, RequestExpander } from './request_expander'; const TS_LANG_DETACH_PORT = 2089; @@ -37,10 +37,19 @@ export class TypescriptServerLauncher extends AbstractLauncher { builtinWorkspace: boolean, maxWorkspace: number ): RequestExpander { - return new RequestExpander(proxy, builtinWorkspace, maxWorkspace, this.options, { - installNodeDependency: this.options.security.installNodeDependency, - gitHostWhitelist: this.options.security.gitHostWhitelist, - }); + return new RequestExpander( + proxy, + builtinWorkspace, + maxWorkspace, + this.options, + { + initialOptions: { + installNodeDependency: this.options.security.installNodeDependency, + gitHostWhitelist: this.options.security.gitHostWhitelist, + }, + } as InitializeOptions, + this.log + ); } async spawnProcess(installationPath: string, port: number, log: Logger): Promise { const p = spawn(process.execPath, [installationPath, '-p', port.toString(), '-c', '1'], { diff --git a/x-pack/plugins/code/server/utils/cancelable.ts b/x-pack/plugins/code/server/utils/cancelable.ts index eb3ba9fc832e9..e6d083d755217 100644 --- a/x-pack/plugins/code/server/utils/cancelable.ts +++ b/x-pack/plugins/code/server/utils/cancelable.ts @@ -24,7 +24,7 @@ export class Cancelable { }); } - public cancel(error = 'canceled'): void { + public cancel(error: any = 'canceled'): void { if (this._cancel) { this._cancel(error); } else if (this.reject) { @@ -37,4 +37,11 @@ export class Cancelable { this.reject(error); } } + + public static fromPromise(promise: Promise) { + return new Cancelable((resolve, reject, c) => { + promise.then(resolve); + promise.catch(reject); + }); + } } diff --git a/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts index 2ea2e55eee8f5..392f945ab00c5 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.test.ts @@ -9,8 +9,8 @@ jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('uuid-v4-id') })); import { EncryptedSavedObjectsClientWrapper } from './encrypted_saved_objects_client_wrapper'; import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service'; import { createEncryptedSavedObjectsServiceMock } from './encrypted_saved_objects_service.mock'; -import { SavedObjectsClientMock } from '../../../../../src/legacy/server/saved_objects/service/saved_objects_client.mock'; -import { SavedObjectsClientContract } from 'src/legacy/server/saved_objects'; +import { SavedObjectsClientMock } from 'src/core/server/saved_objects/service/saved_objects_client.mock'; +import { SavedObjectsClientContract } from 'src/core/server'; let wrapper: EncryptedSavedObjectsClientWrapper; let mockBaseClient: jest.Mocked; diff --git a/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.ts index 35fa46f4e3739..1cef26d08d734 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_client_wrapper.ts @@ -6,19 +6,19 @@ import uuid from 'uuid'; import { - BaseOptions, - BulkCreateObject, - BulkGetObject, - BulkResponse, - CreateOptions, - FindOptions, - FindResponse, + SavedObject, SavedObjectAttributes, + SavedObjectsBaseOptions, + SavedObjectsBulkCreateObject, + SavedObjectsBulkGetObject, + SavedObjectsBulkResponse, SavedObjectsClientContract, - UpdateOptions, - UpdateResponse, - SavedObject, -} from 'src/legacy/server/saved_objects'; + SavedObjectsCreateOptions, + SavedObjectsFindOptions, + SavedObjectsFindResponse, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, +} from 'src/core/server'; import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service'; interface EncryptedSavedObjectsClientOptions { @@ -43,7 +43,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon public async create( type: string, attributes: T = {} as T, - options: CreateOptions = {} + options: SavedObjectsCreateOptions = {} ) { if (!this.options.service.isRegistered(type)) { return await this.options.baseClient.create(type, attributes, options); @@ -71,7 +71,10 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon ); } - public async bulkCreate(objects: BulkCreateObject[], options?: BaseOptions) { + public async bulkCreate( + objects: SavedObjectsBulkCreateObject[], + options?: SavedObjectsBaseOptions + ) { // We encrypt attributes for every object in parallel and that can potentially exhaust libuv or // NodeJS thread pool. If it turns out to be a problem, we can consider switching to the // sequential processing. @@ -107,23 +110,26 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon ); } - public async delete(type: string, id: string, options?: BaseOptions) { + public async delete(type: string, id: string, options?: SavedObjectsBaseOptions) { return await this.options.baseClient.delete(type, id, options); } - public async find(options: FindOptions = {}) { + public async find(options: SavedObjectsFindOptions = {}) { return this.stripEncryptedAttributesFromBulkResponse( await this.options.baseClient.find(options) ); } - public async bulkGet(objects: BulkGetObject[] = [], options?: BaseOptions) { + public async bulkGet( + objects: SavedObjectsBulkGetObject[] = [], + options?: SavedObjectsBaseOptions + ) { return this.stripEncryptedAttributesFromBulkResponse( await this.options.baseClient.bulkGet(objects, options) ); } - public async get(type: string, id: string, options?: BaseOptions) { + public async get(type: string, id: string, options?: SavedObjectsBaseOptions) { return this.stripEncryptedAttributesFromResponse( await this.options.baseClient.get(type, id, options) ); @@ -133,7 +139,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon type: string, id: string, attributes: Partial, - options?: UpdateOptions + options?: SavedObjectsUpdateOptions ) { if (!this.options.service.isRegistered(type)) { return await this.options.baseClient.update(type, id, attributes, options); @@ -157,7 +163,7 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon * registered, response is returned as is. * @param response Raw response returned by the underlying base client. */ - private stripEncryptedAttributesFromResponse( + private stripEncryptedAttributesFromResponse( response: T ): T { if (this.options.service.isRegistered(response.type)) { @@ -175,9 +181,9 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon * response portion isn't registered, it is returned as is. * @param response Raw response returned by the underlying base client. */ - private stripEncryptedAttributesFromBulkResponse( - response: T - ): T { + private stripEncryptedAttributesFromBulkResponse< + T extends SavedObjectsBulkResponse | SavedObjectsFindResponse + >(response: T): T { for (const savedObject of response.saved_objects) { if (this.options.service.isRegistered(savedObject.type)) { savedObject.attributes = this.options.service.stripEncryptedAttributes( diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index 5a7d8ba0f6f34..02b20798afc59 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -6,12 +6,8 @@ import crypto from 'crypto'; import { Legacy, Server } from 'kibana'; -import { SavedObjectsRepository } from 'src/legacy/server/saved_objects/service/lib'; -import { - BaseOptions, - SavedObject, - SavedObjectAttributes, -} from 'src/legacy/server/saved_objects/service/saved_objects_client'; +import { SavedObjectsRepository } from 'src/core/server/saved_objects/service'; +import { SavedObjectsBaseOptions, SavedObject, SavedObjectAttributes } from 'src/core/server'; import { EncryptedSavedObjectsService, EncryptedSavedObjectTypeRegistration, @@ -78,7 +74,7 @@ export class Plugin { getDecryptedAsInternalUser: async ( type: string, id: string, - options?: BaseOptions + options?: SavedObjectsBaseOptions ): Promise> => { const savedObject = await internalRepository.get(type, id, options); return { diff --git a/x-pack/plugins/file_upload/common/constants/file_import.ts b/x-pack/plugins/file_upload/common/constants/file_import.ts new file mode 100644 index 0000000000000..1c82c2b6237e1 --- /dev/null +++ b/x-pack/plugins/file_upload/common/constants/file_import.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const MAX_BYTES = 31457280; + +export const MAX_FILE_SIZE = 52428800; + +// Value to use in the Elasticsearch index mapping metadata to identify the +// index as having been created by the File Upload Plugin. +export const INDEX_META_DATA_CREATED_BY = 'file-upload-plugin'; + +export const ES_GEO_FIELD_TYPE = { + GEO_POINT: 'geo_point', + GEO_SHAPE: 'geo_shape', +}; diff --git a/x-pack/plugins/file_upload/index.js b/x-pack/plugins/file_upload/index.js new file mode 100644 index 0000000000000..24907082adb2c --- /dev/null +++ b/x-pack/plugins/file_upload/index.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; +import { fileUploadRoutes } from './server/routes/file_upload'; +import { makeUsageCollector } from './server/telemetry/'; +import mappings from './mappings'; + +export const fileUpload = kibana => { + return new kibana.Plugin({ + require: ['elasticsearch', 'xpack_main'], + name: 'file_upload', + id: 'file_upload', + uiExports: { + mappings, + }, + savedObjectSchemas: { + 'file-upload-telemetry': { + isNamespaceAgnostic: true + } + }, + + init(server) { + const { xpack_main: xpackMainPlugin } = server.plugins; + + mirrorPluginStatus(xpackMainPlugin, this); + fileUploadRoutes(server); + makeUsageCollector(server); + } + }); +}; diff --git a/x-pack/plugins/file_upload/mappings.json b/x-pack/plugins/file_upload/mappings.json new file mode 100644 index 0000000000000..addff6308d3f0 --- /dev/null +++ b/x-pack/plugins/file_upload/mappings.json @@ -0,0 +1,9 @@ +{ + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + } +} diff --git a/x-pack/plugins/file_upload/public/components/index_settings.js b/x-pack/plugins/file_upload/public/components/index_settings.js new file mode 100644 index 0000000000000..11d388f992824 --- /dev/null +++ b/x-pack/plugins/file_upload/public/components/index_settings.js @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFormRow, + EuiFieldText, + EuiSpacer, + EuiSelect, + EuiCallOut +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getExistingIndices, getExistingIndexPatterns } + from '../util/indexing_service'; + +export class IndexSettings extends Component { + + state = { + indexNameError: '', + indexDisabled: true, + indexPatterns: null, + indexNames: null, + indexName: '', + }; + + componentDidUpdate(prevProps, prevState) { + const { indexNameError, indexName } = this.state; + if (prevState.indexNameError !== indexNameError) { + this.props.setHasIndexErrors(!!indexNameError); + } + const { disabled, indexTypes } = this.props; + const indexDisabled = disabled || !indexTypes || !indexTypes.length; + if (indexDisabled !== this.state.indexDisabled) { + this.setState({ indexDisabled }); + } + if (this.props.indexName !== indexName) { + this._setIndexName(this.props.indexName); + } + } + + async _getIndexNames() { + if (this.state.indexNames) { + return this.state.indexNames; + } + const indices = await getExistingIndices(); + const indexNames = indices + ? indices.map(({ name }) => name) + : []; + this.setState({ indexNames }); + return indexNames; + } + + async _getIndexPatterns() { + if (this.state.indexPatterns) { + return this.state.indexPatterns; + } + const patterns = await getExistingIndexPatterns(); + const indexPatterns = patterns + ? patterns.map(({ name }) => name) + : []; + this.setState({ indexPatterns }); + return indexPatterns; + } + + _setIndexName = async name => { + const errorMessage = await this._isIndexNameAndPatternValid(name); + return this.setState({ + indexName: name, + indexNameError: errorMessage + }); + } + + _onIndexChange = async ({ target }) => { + const name = target.value; + await this._setIndexName(name); + this.props.setIndexName(name); + } + + _isIndexNameAndPatternValid = async name => { + const indexNames = await this._getIndexNames(); + const indexPatterns = await this._getIndexPatterns(); + if (indexNames.find(i => i === name) || indexPatterns.find(i => i === name)) { + return ( + + ); + } + + const reg = new RegExp('[\\\\/\*\?\"\<\>\|\\s\,\#]+'); + if ( + (name !== name.toLowerCase()) || // name should be lowercase + (name === '.' || name === '..') || // name can't be . or .. + name.match(/^[-_+]/) !== null || // name can't start with these chars + name.match(reg) !== null // name can't contain these chars + ) { + return ( + + ); + } + return ''; + } + + render() { + const { setSelectedIndexType, indexTypes } = this.props; + const { indexNameError, indexDisabled, indexName } = this.state; + + return ( + + + + } + > + ({ + text: indexType, + value: indexType, + }))} + onChange={({ target }) => setSelectedIndexType(target.value)} + /> + + + {indexDisabled + ? null + : ( + +
+
    +
  • {i18n.translate('xpack.fileUpload.indexSettings.guidelines.mustBeNewIndex', + { defaultMessage: 'Must be a new index' })} +
  • +
  • {i18n.translate('xpack.fileUpload.indexSettings.guidelines.lowercaseOnly', + { defaultMessage: 'Lowercase only' })} +
  • +
  • {i18n.translate('xpack.fileUpload.indexSettings.guidelines.cannotInclude', + { defaultMessage: 'Cannot include \\\\, /, *, ?, ", <, >, |, \ + " " (space character), , (comma), #' + })} +
  • +
  • {i18n.translate('xpack.fileUpload.indexSettings.guidelines.cannotStartWith', + { defaultMessage: 'Cannot start with -, _, +' })} +
  • +
  • {i18n.translate('xpack.fileUpload.indexSettings.guidelines.cannotBe', + { defaultMessage: 'Cannot be . or ..' })} +
  • +
  • {i18n.translate('xpack.fileUpload.indexSettings.guidelines.length', + { defaultMessage: + 'Cannot be longer than 255 bytes (note it is bytes, \ + so multi-byte characters will count towards the 255 \ + limit faster)' + })} +
  • +
+
+
+ )} + + + } + isInvalid={indexNameError !== ''} + error={[indexNameError]} + > + + + + + +
+ ); + } +} + diff --git a/x-pack/plugins/file_upload/public/components/json_import_progress.js b/x-pack/plugins/file_upload/public/components/json_import_progress.js new file mode 100644 index 0000000000000..46a4c01a31e81 --- /dev/null +++ b/x-pack/plugins/file_upload/public/components/json_import_progress.js @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import React, { Fragment, Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCodeBlock, + EuiSpacer, + EuiFormRow, + EuiText, + EuiProgress, + EuiFlexItem, + EuiCallOut, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import chrome from 'ui/chrome'; + +export class JsonImportProgress extends Component { + + state = { + indexDataJson: null, + indexPatternJson: null, + indexName: '', + importStage: '', + }; + + componentDidUpdate(prevProps, prevState) { + this._setIndex(this.props); + this._formatIndexDataResponse({ ...this.state, ...this.props }); + this._formatIndexPatternResponse({ ...this.state, ...this.props }); + if (prevState.importStage !== this.props.importStage) { + this.setState({ + importStage: this.props.importStage + }); + } + } + + // Retain last index for UI purposes + _setIndex = ({ indexName }) => { + if (indexName && !this.state.indexName) { + this.setState({ indexName }); + } + } + + // Format json responses + _formatIndexDataResponse = ({ indexDataResp, indexDataJson }) => { + if (indexDataResp && !indexDataJson) { + this.setState({ indexDataJson: JSON.stringify(indexDataResp, null, 2) }); + } + } + + _formatIndexPatternResponse = ({ indexPatternResp, indexPatternJson }) => { + if (indexPatternResp && !indexPatternJson) { + this.setState( + { indexPatternJson: JSON.stringify(indexPatternResp, null, 2) } + ); + } + }; + + render() { + const { complete } = this.props; + const { indexPatternJson, indexDataJson, indexName, importStage } = this.state; + const importMessage = complete + ? importStage + : `${importStage}: ${indexName}`; + + return ( + + {!complete ? + : null} + + + } + > + + {importMessage} + + + + {complete + ? ( + + { + indexDataJson + ? ( + + } + > + + {indexDataJson} + + + ) + : null + } + { + indexPatternJson + ? ( + + } + > + + {indexPatternJson} + + + ) + : null + } + + + +
+ { + i18n.translate('xpack.fileUpload.jsonImport.indexModsMsg', + { defaultMessage: 'Further index modifications can be made using\n' + }) + } + + { + i18n.translate('xpack.fileUpload.jsonImport.indexMgmtLink', + { defaultMessage: 'Index Management' }) + } + +
+
+
+
+
+ ) + : null + } + +
+ ); + } +} diff --git a/x-pack/plugins/file_upload/public/components/json_index_file_picker.js b/x-pack/plugins/file_upload/public/components/json_index_file_picker.js new file mode 100644 index 0000000000000..71a4b3ee06db5 --- /dev/null +++ b/x-pack/plugins/file_upload/public/components/json_index_file_picker.js @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import React, { Fragment, Component } from 'react'; +import { + EuiFilePicker, + EuiFormRow, + EuiSpacer, + EuiCallOut, + EuiProgress, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { parseFile } from '../util/file_parser'; +import { MAX_FILE_SIZE } from '../../common/constants/file_import'; + +const ACCEPTABLE_FILETYPES = [ + 'json', + 'geojson', +]; + +export class JsonIndexFilePicker extends Component { + + state = { + fileUploadError: '', + fileParsingProgress: '', + fileRef: null + }; + + componentDidUpdate(prevProps, prevState) { + if (prevState.fileRef !== this.props.fileRef) { + this.setState({ fileRef: this.props.fileRef }); + } + } + + _fileHandler = async fileList => { + const { + resetFileAndIndexSettings, setParsedFile, onFileRemove, onFileUpload, + transformDetails, setFileRef, setIndexName + } = this.props; + + const { fileRef } = this.state; + + resetFileAndIndexSettings(); + this.setState({ fileUploadError: '' }); + if (fileList.length === 0) { // Remove + setParsedFile(null); + if (onFileRemove) { + onFileRemove(fileRef); + } + } else if (fileList.length === 1) { // Parse & index file + const file = fileList[0]; + if (!file.name) { + this.setState({ + fileUploadError: i18n.translate( + 'xpack.fileUpload.jsonIndexFilePicker.noFileNameError', + { defaultMessage: 'No file name provided' }) + }); + return; + } + + // Check file type, assign default index name + const splitNameArr = file.name.split('.'); + const fileType = splitNameArr.pop(); + const types = ACCEPTABLE_FILETYPES.reduce((accu, type) => { + accu = accu ? `${accu}, ${type}` : type; + return accu; + }, ''); + if (!ACCEPTABLE_FILETYPES.includes(fileType)) { + this.setState({ + fileUploadError: ( + + ) + }); + return; + } + const initIndexName = splitNameArr[0]; + setIndexName(initIndexName); + + // Check valid size + const { size } = file; + if (size > MAX_FILE_SIZE) { + this.setState({ + fileUploadError: ( + + ) + }); + return; + } + + // Parse file + this.setState({ fileParsingProgress: i18n.translate( + 'xpack.fileUpload.jsonIndexFilePicker.parsingFile', + { defaultMessage: 'Parsing file...' }) + }); + const parsedFileResult = await parseFile( + file, onFileUpload, transformDetails + ).catch(err => { + this.setState({ + fileUploadError: ( + + ) + }); + }); + this.setState({ fileParsingProgress: '' }); + if (!parsedFileResult) { + if (fileRef) { + if (onFileRemove) { + onFileRemove(fileRef); + } + setFileRef(null); + } + return; + } + setFileRef(file); + setParsedFile(parsedFileResult); + + } else { + // No else + } + } + + render() { + const { fileParsingProgress, fileUploadError, fileRef } = this.state; + + return ( + + { fileParsingProgress + ? + : null + } + { + fileRef && !fileUploadError + ? null + : ( + +
+
    +
  • + { + i18n.translate( + 'xpack.fileUpload.jsonIndexFilePicker.formatsAccepted', + { defaultMessage: 'Formats accepted: .json, .geojson' } + ) + } +
  • +
  • + +
  • +
+
+
+ ) + } + + + )} + isInvalid={fileUploadError !== ''} + error={[fileUploadError]} + helpText={fileParsingProgress} + > + + )} + onChange={this._fileHandler} + /> + +
+ ); + } +} + +function bytesToSize(bytes) { + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) return 'n/a'; + const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); + if (i === 0) return `${bytes} ${sizes[i]})`; + return `${(bytes / (1024 ** i)).toFixed(1)} ${sizes[i]}`; +} diff --git a/x-pack/plugins/file_upload/public/components/json_upload_and_parse.js b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.js new file mode 100644 index 0000000000000..f2f869a338999 --- /dev/null +++ b/x-pack/plugins/file_upload/public/components/json_upload_and_parse.js @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiForm, +} from '@elastic/eui'; +import PropTypes from 'prop-types'; +import { indexData, createIndexPattern } from '../util/indexing_service'; +import { getGeoIndexTypesForFeatures } from '../util/geo_processing'; +import { IndexSettings } from './index_settings'; +import { JsonIndexFilePicker } from './json_index_file_picker'; +import { JsonImportProgress } from './json_import_progress'; +import _ from 'lodash'; + +const INDEXING_STAGE = { + INDEXING_STARTED: i18n.translate( + 'xpack.fileUpload.jsonUploadAndParse.dataIndexingStarted', + { defaultMessage: 'Data indexing started' }), + WRITING_TO_INDEX: i18n.translate( + 'xpack.fileUpload.jsonUploadAndParse.writingToIndex', + { defaultMessage: 'Writing to index' }), + CREATING_INDEX_PATTERN: i18n.translate( + 'xpack.fileUpload.jsonUploadAndParse.creatingIndexPattern', + { defaultMessage: 'Creating index pattern' }), + INDEX_PATTERN_COMPLETE: i18n.translate( + 'xpack.fileUpload.jsonUploadAndParse.indexPatternComplete', + { defaultMessage: 'Index pattern complete' }), + DATA_INDEXING_ERROR: i18n.translate( + 'xpack.fileUpload.jsonUploadAndParse.dataIndexingError', + { defaultMessage: 'Data indexing error' }), + INDEX_PATTERN_ERROR: i18n.translate( + 'xpack.fileUpload.jsonUploadAndParse.indexPatternError', + { defaultMessage: 'Index pattern error' }), +}; + +export class JsonUploadAndParse extends Component { + + state = { + // File state + fileRef: null, + parsedFile: null, + indexedFile: null, + + // Index state + indexTypes: [], + selectedIndexType: '', + indexName: '', + indexRequestInFlight: false, + indexPatternRequestInFlight: false, + hasIndexErrors: false, + isIndexReady: false, + + // Progress-tracking state + showImportProgress: false, + currentIndexingStage: INDEXING_STAGE.INDEXING_STARTED, + indexDataResp: '', + indexPatternResp: '', + }; + + _resetFileAndIndexSettings = () => { + this.setState({ + indexTypes: [], + selectedIndexType: '', + indexName: '', + indexedFile: null, + parsedFile: null, + fileRef: null, + }); + }; + + componentDidUpdate(prevProps, prevState) { + if (!_.isEqual(prevState.parsedFile, this.state.parsedFile)) { + this._setIndexTypes({ ...this.state, ...this.props }); + } + this._setSelectedType(this.state); + this._setIndexReady({ ...this.state, ...this.props }); + this._indexData({ ...this.state, ...this.props }); + if (this.props.isIndexingTriggered && !this.state.showImportProgress) { + this.setState({ showImportProgress: true }); + } + } + + _setSelectedType = ({ selectedIndexType, indexTypes }) => { + if (!selectedIndexType && indexTypes.length) { + this.setState({ selectedIndexType: indexTypes[0] }); + } + } + + _setIndexReady = ({ + parsedFile, selectedIndexType, indexName, hasIndexErrors, + indexRequestInFlight, onIndexReady + }) => { + const isIndexReady = !!parsedFile && !!selectedIndexType && + !!indexName && !hasIndexErrors && !indexRequestInFlight; + if (isIndexReady !== this.state.isIndexReady) { + this.setState({ isIndexReady }); + if (onIndexReady) { + onIndexReady(isIndexReady); + } + } + } + + _indexData = async ({ + indexedFile, parsedFile, indexRequestInFlight, transformDetails, + indexName, appName, selectedIndexType, isIndexingTriggered, isIndexReady, + onIndexingComplete, boolCreateIndexPattern + }) => { + // Check index ready + const filesAreEqual = _.isEqual(indexedFile, parsedFile); + if (!isIndexingTriggered || filesAreEqual || !isIndexReady || indexRequestInFlight) { + return; + } + this.setState({ + indexRequestInFlight: true, + currentIndexingStage: INDEXING_STAGE.WRITING_TO_INDEX + }); + + // Index data + const indexDataResp = await indexData( + parsedFile, transformDetails, indexName, selectedIndexType, appName + ); + + // Index error + if (!indexDataResp.success) { + this.setState({ + indexedFile: null, + indexDataResp, + indexRequestInFlight: false, + currentIndexingStage: INDEXING_STAGE.INDEXING_COMPLETE, + }); + this._resetFileAndIndexSettings(); + if (onIndexingComplete) { + onIndexingComplete(); + } + return; + } + + // Index data success. Update state & create index pattern + this.setState({ + indexDataResp, + indexedFile: parsedFile, + }); + let indexPatternResp; + if (boolCreateIndexPattern) { + indexPatternResp = await this._createIndexPattern(this.state); + } + + // Indexing complete, update state & callback (if any) + this.setState({ currentIndexingStage: INDEXING_STAGE.INDEXING_COMPLETE }); + if (onIndexingComplete) { + onIndexingComplete({ + indexDataResp, + ...(boolCreateIndexPattern ? { indexPatternResp } : {}) + }); + } + } + + _createIndexPattern = async ({ indexName }) => { + this.setState({ + indexPatternRequestInFlight: true, + currentIndexingStage: INDEXING_STAGE.CREATING_INDEX_PATTERN + }); + const indexPatternResp = await createIndexPattern(indexName); + + this.setState({ + indexPatternResp, + indexPatternRequestInFlight: false, + }); + this._resetFileAndIndexSettings(); + + return indexPatternResp; + } + + // This is mostly for geo. Some data have multiple valid index types that can + // be chosen from, such as 'geo_point' vs. 'geo_shape' for point data + _setIndexTypes = ({ transformDetails, parsedFile }) => { + if (parsedFile) { + // User-provided index types + if (typeof transformDetails === 'object') { + this.setState({ indexTypes: transformDetails.indexTypes }); + } else { + // Included index types + switch (transformDetails) { + case 'geo': + const featureTypes = _.uniq( + parsedFile.features + ? parsedFile.features.map(({ geometry }) => geometry.type) + : [ parsedFile.geometry.type ] + ); + this.setState({ + indexTypes: getGeoIndexTypesForFeatures(featureTypes) + }); + break; + default: + this.setState({ indexTypes: [] }); + return; + } + } + } + } + + render() { + const { + currentIndexingStage, indexDataResp, indexPatternResp, fileRef, + indexName, indexTypes, showImportProgress + } = this.state; + const { onFileUpload, onFileRemove, transformDetails } = this.props; + + return ( + + {showImportProgress + ? + : ( + + this.setState({ indexName }), + setFileRef: fileRef => this.setState({ fileRef }), + setParsedFile: parsedFile => this.setState({ parsedFile }), + transformDetails, + resetFileAndIndexSettings: this._resetFileAndIndexSettings, + }} + /> + this.setState({ indexName })} + indexTypes={indexTypes} + setSelectedIndexType={selectedIndexType => + this.setState({ selectedIndexType }) + } + setHasIndexErrors={hasIndexErrors => + this.setState({ hasIndexErrors }) + } + /> + + ) + } + + ); + } +} + +JsonUploadAndParse.defaultProps = { + isIndexingTriggered: false, + boolCreateIndexPattern: true, +}; + +JsonUploadAndParse.propTypes = { + appName: PropTypes.string, + isIndexingTriggered: PropTypes.bool, + boolCreateIndexPattern: PropTypes.bool, + transformDetails: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]), + onIndexReadyStatusChange: PropTypes.func, + onIndexingComplete: PropTypes.func, + onFileUpload: PropTypes.func +}; diff --git a/x-pack/plugins/file_upload/public/index.js b/x-pack/plugins/file_upload/public/index.js new file mode 100644 index 0000000000000..a02b82170f70f --- /dev/null +++ b/x-pack/plugins/file_upload/public/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JsonUploadAndParse } from './components/json_upload_and_parse'; diff --git a/x-pack/plugins/file_upload/public/kibana_services.js b/x-pack/plugins/file_upload/public/kibana_services.js new file mode 100644 index 0000000000000..8dd2923752b99 --- /dev/null +++ b/x-pack/plugins/file_upload/public/kibana_services.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { uiModules } from 'ui/modules'; + +export let indexPatternService; + +uiModules.get('app/file_upload').run(($injector) => { + indexPatternService = $injector.get('indexPatterns'); +}); diff --git a/x-pack/plugins/file_upload/public/util/file_parser.js b/x-pack/plugins/file_upload/public/util/file_parser.js new file mode 100644 index 0000000000000..7cd2c5a49a04d --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/file_parser.js @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { geoJsonCleanAndValidate } from './geo_json_clean_and_validate'; +import { i18n } from '@kbn/i18n'; + +export async function parseFile(file, previewCallback = null, transformDetails, + FileReader = window.FileReader) { + + let cleanAndValidate; + if (typeof transformDetails === 'object') { + cleanAndValidate = transformDetails.cleanAndValidate; + } else { + switch(transformDetails) { + case 'geo': + cleanAndValidate = geoJsonCleanAndValidate; + break; + default: + throw( + i18n.translate( + 'xpack.fileUpload.fileParser.transformDetailsNotDefined', { + defaultMessage: 'Index options for {transformDetails} not defined', + values: { transformDetails } + }) + ); + return; + } + } + + return new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onload = ({ target: { result } }) => { + try { + const parsedJson = JSON.parse(result); + // Clean & validate + const cleanAndValidJson = cleanAndValidate(parsedJson); + if (!cleanAndValidJson) { + return; + } + if (previewCallback) { + const defaultName = _.get(cleanAndValidJson, 'name', 'Import File'); + previewCallback(cleanAndValidJson, defaultName); + } + resolve(cleanAndValidJson); + } catch (e) { + reject(e); + } + }; + fr.readAsText(file); + }); +} diff --git a/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js new file mode 100644 index 0000000000000..a2933a9f01dd8 --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.js @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const jsts = require('jsts'); +import rewind from 'geojson-rewind'; + +export function geoJsonCleanAndValidate(parsedFile) { + + const reader = new jsts.io.GeoJSONReader(); + const geoJson = reader.read(parsedFile); + const isSingleFeature = parsedFile.type === 'Feature'; + const features = isSingleFeature + ? [{ ...geoJson }] + : geoJson.features; + + // Pass features for cleaning + const cleanedFeatures = cleanFeatures(features); + + // Put clean features back in geoJson object + const cleanGeoJson = { + ...parsedFile, + ...(isSingleFeature + ? cleanedFeatures[0] + : { features: cleanedFeatures } + ), + }; + + // Pass entire geoJson object for winding + // JSTS does not enforce winding order, wind in clockwise order + const correctlyWindedGeoJson = rewind(cleanGeoJson, false); + return correctlyWindedGeoJson; +} + +export function cleanFeatures(features) { + const writer = new jsts.io.GeoJSONWriter(); + return features.map(({ id, geometry, properties }) => { + const geojsonGeometry = (geometry.isSimple() || geometry.isValid()) + ? writer.write(geometry) + : writer.write(geometry.buffer(0)); + return ({ + type: 'Feature', + geometry: geojsonGeometry, + ...(id ? { id } : {}), + ...(properties ? { properties } : {}), + }); + }); +} diff --git a/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js new file mode 100644 index 0000000000000..5c25120a7f5de --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/geo_json_clean_and_validate.test.js @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + cleanFeatures, + geoJsonCleanAndValidate, +} from './geo_json_clean_and_validate'; +const jsts = require('jsts'); + +describe('geo_json_clean_and_validate', () => { + + const reader = new jsts.io.GeoJSONReader(); + + it('should not modify valid features', () => { + const goodFeatureGeoJson = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [-104.05, 78.99], + [-87.22, 78.98], + [-86.58, 75.94], + [-104.03, 75.94], + [-104.05, 78.99] + ]] + }, + }; + + // Confirm valid geometry + const geoJson = reader.read(goodFeatureGeoJson); + const isSimpleOrValid = (geoJson.geometry.isSimple() + || geoJson.geometry.isValid()); + expect(isSimpleOrValid).toEqual(true); + + // Confirm no change to features + const cleanedFeatures = cleanFeatures([geoJson]); + expect(cleanedFeatures[0]).toEqual(goodFeatureGeoJson); + }); + + it('should modify incorrect features', () => { + // This feature collection contains polygons which cross over themselves, + // which is invalid for geojson + const badFeaturesGeoJson = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [0, 0], + [2, 2], + [0, 2], + [2, 0], + [0, 0] + ]] + } + }, + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [2, 2], + [4, 0], + [2, 0], + [4, 2], + [2, 2] + ]] + } + } + ] + }; + + // Confirm invalid geometry + let geoJson = reader.read(badFeaturesGeoJson); + let isSimpleOrValid; + geoJson.features.forEach(feature => { + isSimpleOrValid = (feature.geometry.isSimple() + || feature.geometry.isValid()); + expect(isSimpleOrValid).toEqual(false); + }); + + // Confirm changes to object + const cleanedFeatures = cleanFeatures(geoJson.features); + expect(cleanedFeatures).not.toEqual(badFeaturesGeoJson.features); + + // Confirm now valid features geometry + geoJson = reader.read({ ...badFeaturesGeoJson, features: cleanedFeatures }); + geoJson.features.forEach(feature => { + isSimpleOrValid = (feature.geometry.isSimple() + || feature.geometry.isValid()); + expect(isSimpleOrValid).toEqual(true); + }); + }); + + it('should reverse counter-clockwise winding order', () => { + const counterClockwiseGeoJson = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [100, 0], + [101, 0], + [101, 1], + [100, 1], + [100, 0] + ], [ + [100.2, 0.2], + [100.8, 0.2], + [100.8, 0.8], + [100.2, 0.8], + [100.2, 0.2] + ]] + } + }; + + // Confirm changes to object + const clockwiseGeoJson = geoJsonCleanAndValidate(counterClockwiseGeoJson); + expect(clockwiseGeoJson).not.toEqual(counterClockwiseGeoJson); + + // Run it through again, expect it not to change + const clockwiseGeoJson2 = geoJsonCleanAndValidate(clockwiseGeoJson); + expect(clockwiseGeoJson).toEqual(clockwiseGeoJson2); + }); + + it('error out on invalid object', () => { + const invalidGeoJson = { + type: 'notMyType', + geometry: 'shmeometry' + }; + + const notEvenCloseToGeoJson = [1, 2, 3, 4]; + + const badObjectPassed = () => geoJsonCleanAndValidate(invalidGeoJson); + expect(badObjectPassed).toThrow(); + + const worseObjectPassed = () => geoJsonCleanAndValidate(notEvenCloseToGeoJson); + expect(worseObjectPassed).toThrow(); + }); +}); diff --git a/x-pack/plugins/file_upload/public/util/geo_processing.js b/x-pack/plugins/file_upload/public/util/geo_processing.js new file mode 100644 index 0000000000000..469fbfb6f4a94 --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/geo_processing.js @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { ES_GEO_FIELD_TYPE } from '../../common/constants/file_import'; +import { i18n } from '@kbn/i18n'; + +const DEFAULT_SETTINGS = { + number_of_shards: 1 +}; + +const DEFAULT_GEO_SHAPE_MAPPINGS = { + 'coordinates': { + 'type': ES_GEO_FIELD_TYPE.GEO_SHAPE + } +}; + +const DEFAULT_GEO_POINT_MAPPINGS = { + 'coordinates': { + 'type': ES_GEO_FIELD_TYPE.GEO_POINT + } +}; + +const DEFAULT_INGEST_PIPELINE = {}; + +export function getGeoIndexTypesForFeatures(featureTypes) { + if (!featureTypes || !featureTypes.length) { + return []; + } else if (!featureTypes.includes('Point')) { + return [ES_GEO_FIELD_TYPE.GEO_SHAPE]; + } else if (featureTypes.includes('Point') && featureTypes.length === 1) { + return [ ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE ]; + } else { + return [ ES_GEO_FIELD_TYPE.GEO_SHAPE ]; + } +} + +// Reduces & flattens geojson to coordinates and properties (if any) +export function geoJsonToEs(parsedGeojson, datatype) { + if (!parsedGeojson) { + return []; + } + const features = parsedGeojson.type === 'Feature' + ? [ parsedGeojson ] + : parsedGeojson.features; + + if (datatype === ES_GEO_FIELD_TYPE.GEO_SHAPE) { + return features.reduce((accu, { geometry, properties }) => { + const { coordinates } = geometry; + accu.push({ + coordinates: { + 'type': geometry.type.toLowerCase(), + 'coordinates': coordinates + }, + ...(!_.isEmpty(properties) ? { ...properties } : {}) + }); + return accu; + }, []); + } else if (datatype === ES_GEO_FIELD_TYPE.GEO_POINT) { + return features.reduce((accu, { geometry, properties }) => { + const { coordinates } = geometry; + if (Array.isArray(coordinates[0])) { + throw( + i18n.translate( + 'xpack.fileUpload.geoProcessing.notPointError', { + defaultMessage: 'Coordinates {coordinates} does not contain point datatype', + values: { coordinates: coordinates.toString() } + }) + ); + return accu; + } + accu.push({ + coordinates, + ...(!_.isEmpty(properties) ? { ...properties } : {}) + }); + return accu; + }, []); + } else { + return []; + } +} + +export function getGeoJsonIndexingDetails(parsedGeojson, dataType) { + return { + data: geoJsonToEs(parsedGeojson, dataType), + ingestPipeline: DEFAULT_INGEST_PIPELINE, + mappings: (dataType === ES_GEO_FIELD_TYPE.GEO_POINT) + ? DEFAULT_GEO_POINT_MAPPINGS + : DEFAULT_GEO_SHAPE_MAPPINGS, + settings: DEFAULT_SETTINGS + }; +} diff --git a/x-pack/plugins/file_upload/public/util/geo_processing.test.js b/x-pack/plugins/file_upload/public/util/geo_processing.test.js new file mode 100644 index 0000000000000..f3cda7de06213 --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/geo_processing.test.js @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { geoJsonToEs } from './geo_processing'; +import { ES_GEO_FIELD_TYPE } from '../../common/constants/file_import'; + +describe('geo_processing', () => { + describe('getGeoJsonToEs', () => { + + const parsedPointFeature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [105.7, 18.9] + }, + properties: { + name: 'Dogeville' + } + }; + + it('should convert point feature to flattened ES compatible feature', () => { + const esFeatureArr = geoJsonToEs(parsedPointFeature, ES_GEO_FIELD_TYPE.GEO_POINT); + expect(esFeatureArr).toEqual([{ + coordinates: [ + 105.7, + 18.9 + ], + name: 'Dogeville', + }]); + }); + + it('should convert point feature collection to flattened ES compatible feature', () => { + const parsedPointFeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [34.1, 15.3] + }, + properties: { + name: 'Meowsers City' + } + } + ] + }; + + const esFeatureArr = geoJsonToEs( + parsedPointFeatureCollection, + ES_GEO_FIELD_TYPE.GEO_POINT + ); + expect(esFeatureArr).toEqual([{ + coordinates: [ + 34.1, + 15.3, + ], + name: 'Meowsers City', + }]); + }); + + it('should convert shape feature to flattened ES compatible feature', () => { + const parsedShapeFeature = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [-104.05, 78.99], + [-87.22, 78.98], + [-86.58, 75.94], + [-104.03, 75.94], + [-104.05, 78.99] + ]] + }, + properties: { + name: 'Whiskers City' + } + }; + + const esFeatureArr = geoJsonToEs(parsedShapeFeature, ES_GEO_FIELD_TYPE.GEO_SHAPE); + expect(esFeatureArr).toEqual([{ + coordinates: { + coordinates: [[ + [-104.05, 78.99], + [-87.22, 78.98], + [-86.58, 75.94], + [-104.03, 75.94], + [-104.05, 78.99], + ]], + type: 'polygon' + }, + name: 'Whiskers City', + }]); + }); + + it('should convert shape feature collection to flattened ES compatible feature', () => { + + const parsedShapeFeatureCollection = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [[ + [-104.05, 79.89], + [-87.22, 79.88], + [-86.58, 74.84], + [-104.03, 75.84], + [-104.05, 78.89] + ]] + }, + properties: { + name: 'Woof Crossing' + } + } + ] + }; + + const esFeatureArr = geoJsonToEs( + parsedShapeFeatureCollection, + ES_GEO_FIELD_TYPE.GEO_SHAPE + ); + expect(esFeatureArr).toEqual([{ + coordinates: { + coordinates: [[ + [-104.05, 79.89], + [-87.22, 79.88], + [-86.58, 74.84], + [-104.03, 75.84], + [-104.05, 78.89] + ]], + type: 'polygon', + }, + name: 'Woof Crossing', + }]); + }); + + it('should return an empty for an unhandled datatype', () => { + const esFeatureArr = geoJsonToEs(parsedPointFeature, 'different datatype'); + expect(esFeatureArr).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/file_upload/public/util/http_service.js b/x-pack/plugins/file_upload/public/util/http_service.js new file mode 100644 index 0000000000000..44a6a0b31c7a6 --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/http_service.js @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +// service for interacting with the server + +import chrome from 'ui/chrome'; +import { addSystemApiHeader } from 'ui/system_api'; +import { i18n } from '@kbn/i18n'; + +const FETCH_TIMEOUT = 10000; + +export async function http(options) { + if(!(options && options.url)) { + throw( + i18n.translate('xpack.fileUpload.httpService.noUrl', + { defaultMessage: 'No URL provided' }) + ); + } + const url = options.url || ''; + const headers = addSystemApiHeader({ + 'Content-Type': 'application/json', + 'kbn-version': chrome.getXsrfToken(), + ...options.headers + }); + + const allHeaders = (options.headers === undefined) ? headers : { ...options.headers, ...headers }; + const body = (options.data === undefined) ? null : JSON.stringify(options.data); + + const payload = { + method: (options.method || 'GET'), + headers: allHeaders, + credentials: 'same-origin' + }; + + if (body !== null) { + payload.body = body; + } + return await fetchWithTimeout(url, payload); +} + +async function fetchWithTimeout(url, payload) { + let timedOut = false; + + return new Promise(function (resolve, reject) { + const timeout = setTimeout(function () { + timedOut = true; + reject(new Error( + i18n.translate('xpack.fileUpload.httpService.requestTimedOut', + { defaultMessage: 'Request timed out' })) + ); + }, FETCH_TIMEOUT); + + fetch(url, payload) + .then(resp => { + clearTimeout(timeout); + if (!timedOut) { + resolve(resp); + } + }) + .catch(function (err) { + reject(err); + if (timedOut) return; + }); + }).then(resp => resp.json()) + .catch(function (err) { + console.error( + i18n.translate('xpack.fileUpload.httpService.fetchError', { + defaultMessage: 'Error performing fetch: {error}', + values: { error: err.message } + })); + }); +} diff --git a/x-pack/plugins/file_upload/public/util/indexing_service.js b/x-pack/plugins/file_upload/public/util/indexing_service.js new file mode 100644 index 0000000000000..bd8eb843787ea --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/indexing_service.js @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { http } from './http_service'; +import chrome from 'ui/chrome'; +import { i18n } from '@kbn/i18n'; +import { indexPatternService } from '../kibana_services'; +import { getGeoJsonIndexingDetails } from './geo_processing'; +import { sizeLimitedChunking } from './size_limited_chunking'; + +const basePath = chrome.addBasePath('/api/fileupload'); +const fileType = 'json'; + +export async function indexData(parsedFile, transformDetails, indexName, dataType, appName) { + if (!parsedFile) { + throw(i18n.translate('xpack.fileUpload.indexingService.noFileImported', { + defaultMessage: 'No file imported.' + })); + return; + } + + // Perform any processing required on file prior to indexing + const transformResult = transformDataByFormatForIndexing(transformDetails, parsedFile, dataType); + if (!transformResult.success) { + throw(i18n.translate('xpack.fileUpload.indexingService.transformResultError', { + defaultMessage: 'Error transforming data: {error}', + values: { error: transformResult.error } + })); + } + + // Create new index + const { indexingDetails } = transformResult; + const createdIndex = await writeToIndex({ + appName, + ...indexingDetails, + id: undefined, + data: [], + index: indexName, + }); + let id; + try { + if (createdIndex && createdIndex.id) { + id = createdIndex.id; + } else { + throw i18n.translate('xpack.fileUpload.indexingService.errorCreatingIndex', { + defaultMessage: 'Error creating index', + }); + } + } catch (error) { + return { + error, + success: false + }; + } + + // Write to index + const indexWriteResults = await chunkDataAndWriteToIndex({ + id, + index: indexName, + ...indexingDetails, + settings: {}, + mappings: {}, + }); + return indexWriteResults; +} + + +function transformDataByFormatForIndexing(transform, parsedFile, dataType) { + let indexingDetails; + if (!transform) { + return { + success: false, + error: i18n.translate('xpack.fileUpload.indexingService.noTransformDefined', { + defaultMessage: 'No transform defined', + }) + }; + } + if (typeof transform !== 'object') { + switch(transform) { + case 'geo': + indexingDetails = getGeoJsonIndexingDetails(parsedFile, dataType); + break; + default: + return { + success: false, + error: i18n.translate('xpack.fileUpload.indexingService.noHandlingForTransform', { + defaultMessage: 'No handling defined for transform: {transform}', + values: { transform } + }) + }; + } + } else { // Custom transform + indexingDetails = transform.getIndexingDetails(parsedFile); + } + if (indexingDetails && indexingDetails.data && indexingDetails.data.length) { + return { + success: true, + indexingDetails + }; + } else if (indexingDetails && indexingDetails.data) { + return { + success: false, + error: i18n.translate('xpack.fileUpload.indexingService.noIndexingDetailsForDatatype', { + defaultMessage: `No indexing details defined for datatype: {dataType}`, + values: { dataType } + }) + }; + } else { + return { + success: false, + error: i18n.translate('xpack.fileUpload.indexingService.unknownTransformError', { + defaultMessage: 'Unknown error performing transform: {transform}', + values: { transform } + }) + }; + } +} + +async function writeToIndex(indexingDetails) { + const paramString = (indexingDetails.id !== undefined) ? `?id=${indexingDetails.id}` : ''; + const { + appName, + index, + data, + settings, + mappings, + ingestPipeline + } = indexingDetails; + + return await http({ + url: `${basePath}/import${paramString}`, + method: 'POST', + data: { + index, + data, + settings, + mappings, + ingestPipeline, + fileType, + ...(appName ? { app: appName } : {}) + }, + }); +} + +async function chunkDataAndWriteToIndex({ id, index, data, mappings, settings }) { + if (!index) { + return { + success: false, + error: i18n.translate('xpack.fileUpload.noIndexSuppliedErrorMessage', { + defaultMessage: 'No index provided.' + }) + }; + } + + const chunks = sizeLimitedChunking(data); + + let success = true; + let failures = []; + let error; + let docCount = 0; + + for (let i = 0; i < chunks.length; i++) { + const aggs = { + id, + index, + data: chunks[i], + settings, + mappings, + ingestPipeline: {} // TODO: Support custom ingest pipelines + }; + + let resp = { + success: false, + failures: [], + docCount: 0, + }; + resp = await writeToIndex(aggs); + + failures = [ ...failures, ...resp.failures ]; + if (resp.success) { + ({ success } = resp); + docCount = docCount + resp.docCount; + } else { + success = false; + error = resp.error; + docCount = 0; + break; + } + } + + return { + success, + failures, + docCount, + ...(error ? { error } : {}) + }; +} + +export async function createIndexPattern(indexPatternName) { + const indexPatterns = await indexPatternService.get(); + try { + Object.assign(indexPatterns, { + id: '', + title: indexPatternName, + }); + + await indexPatterns.create(true); + const id = await getIndexPatternId(indexPatternName); + const indexPattern = await indexPatternService.get(id); + return { + success: true, + id, + fields: indexPattern.fields + }; + } catch (error) { + return { + success: false, + error, + }; + } +} + +async function getIndexPatternId(name) { + const savedObjectsClient = chrome.getSavedObjectsClient(); + const savedObjectSearch = + await savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); + const indexPatternSavedObjects = savedObjectSearch.savedObjects; + + if (indexPatternSavedObjects) { + const ip = indexPatternSavedObjects.find(i => i.attributes.title === name); + return (ip !== undefined) ? ip.id : undefined; + } else { + return undefined; + } +} + +export async function getExistingIndices() { + const basePath = chrome.addBasePath('/api'); + return await http({ + url: `${basePath}/index_management/indices`, + method: 'GET', + }); +} + +export async function getExistingIndexPatterns() { + const savedObjectsClient = chrome.getSavedObjectsClient(); + return savedObjectsClient.find({ + type: 'index-pattern', + fields: ['id', 'title', 'type', 'fields'], + perPage: 10000 + }).then(({ savedObjects }) => + savedObjects.map(savedObject => savedObject.get('title')) + ); +} diff --git a/x-pack/plugins/file_upload/public/util/size_limited_chunking.js b/x-pack/plugins/file_upload/public/util/size_limited_chunking.js new file mode 100644 index 0000000000000..4c49772b9ddce --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/size_limited_chunking.js @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { MAX_BYTES } from '../../common/constants/file_import'; + +// Add data elements to chunk until limit is met +export function sizeLimitedChunking(dataArr, maxChunkSize = MAX_BYTES) { + let chunkSize = 0; + return dataArr.reduce((accu, el) => { + const featureByteSize = ( + new Blob([JSON.stringify(el)], { type: 'application/json' }) + ).size; + if (featureByteSize > maxChunkSize) { + throw `Some features exceed maximum chunk size of ${maxChunkSize}`; + } else if (chunkSize + featureByteSize < maxChunkSize) { + const lastChunkRef = accu.length - 1; + chunkSize += featureByteSize; + accu[lastChunkRef].push(el); + } else { + chunkSize = featureByteSize; + accu.push([el]); + } + return accu; + }, [[]]); +} + + diff --git a/x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js b/x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js new file mode 100644 index 0000000000000..12e2cb35e8b78 --- /dev/null +++ b/x-pack/plugins/file_upload/public/util/size_limited_chunking.test.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + sizeLimitedChunking +} from './size_limited_chunking'; + +describe('size_limited_chunking', () => { + + // 1000 elements where element value === index + const testArr = Array.from(Array(1000), (_, x) => x); + + it('should limit each sub-array to the max chunk size', () => { + // Confirm valid geometry + const chunkLimit = 100; + const chunkedArr = sizeLimitedChunking(testArr, chunkLimit); + chunkedArr.forEach(sizeLimitedArr => { + const arrByteSize = ( + new Blob(sizeLimitedArr, { type: 'application/json' }) + ).size; + + // Chunk size should be less than chunk limit + expect(arrByteSize).toBeLessThan(chunkLimit); + // # of arrays generated should be greater than original array length + // divided by chunk limit + expect(chunkedArr.length).toBeGreaterThanOrEqual(testArr.length / chunkLimit); + }); + }); +}); diff --git a/x-pack/plugins/ml/server/client/log.js b/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts similarity index 64% rename from x-pack/plugins/ml/server/client/log.js rename to x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts index a06031ab0868b..0b39c81cee6ff 100644 --- a/x-pack/plugins/ml/server/client/log.js +++ b/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.d.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export let mlLog = () => {}; +import { Server } from 'hapi'; -export function initMlServerLog(server) { - mlLog = (level, message) => server.log(['ml', level], message); -} +export function callWithInternalUserFactory(server: Server): any; diff --git a/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.js b/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.js new file mode 100644 index 0000000000000..dc3131484e75f --- /dev/null +++ b/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import { once } from 'lodash'; + +const _callWithInternalUser = once((server) => { + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + return callWithInternalUser; +}); + +export const callWithInternalUserFactory = (server) => { + return (...args) => { + return _callWithInternalUser(server)(...args); + }; +}; diff --git a/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts b/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts new file mode 100644 index 0000000000000..d77541e7d3d6c --- /dev/null +++ b/x-pack/plugins/file_upload/server/client/call_with_internal_user_factory.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { callWithInternalUserFactory } from './call_with_internal_user_factory'; + +describe('call_with_internal_user_factory', () => { + describe('callWithInternalUserFactory', () => { + let server: any; + let callWithInternalUser: any; + + beforeEach(() => { + callWithInternalUser = jest.fn(); + server = { + plugins: { + elasticsearch: { + getCluster: jest.fn(() => ({ callWithInternalUser })), + }, + }, + }; + }); + + it('should use internal user "admin"', () => { + const callWithInternalUserInstance = callWithInternalUserFactory(server); + callWithInternalUserInstance(); + + expect(server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('admin'); + }); + }); +}); diff --git a/x-pack/plugins/file_upload/server/client/call_with_request_factory.js b/x-pack/plugins/file_upload/server/client/call_with_request_factory.js new file mode 100644 index 0000000000000..0040fcb6c802a --- /dev/null +++ b/x-pack/plugins/file_upload/server/client/call_with_request_factory.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import { once } from 'lodash'; + +const callWithRequest = once((server) => { + const cluster = server.plugins.elasticsearch.getCluster('data'); + return cluster.callWithRequest; +}); + +export const callWithRequestFactory = (server, request) => { + return (...args) => { + return callWithRequest(server)(request, ...args); + }; +}; diff --git a/x-pack/plugins/file_upload/server/client/errors.js b/x-pack/plugins/file_upload/server/client/errors.js new file mode 100644 index 0000000000000..98da148192a89 --- /dev/null +++ b/x-pack/plugins/file_upload/server/client/errors.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import { boomify } from 'boom'; + +export function wrapError(error) { + return boomify(error, { statusCode: error.status }); +} diff --git a/x-pack/plugins/file_upload/server/models/import_data/import_data.js b/x-pack/plugins/file_upload/server/models/import_data/import_data.js new file mode 100644 index 0000000000000..4311ac7f2a388 --- /dev/null +++ b/x-pack/plugins/file_upload/server/models/import_data/import_data.js @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_import'; +import uuid from 'uuid'; + +export function importDataProvider(callWithRequest) { + async function importData(id, index, settings, mappings, ingestPipeline, data) { + let createdIndex; + let createdPipelineId; + const docCount = data.length; + + try { + + const { + id: pipelineId, + pipeline, + } = ingestPipeline; + + if (id === undefined) { + // first chunk of data, create the index and id to return + id = uuid.v1(); + + await createIndex(index, settings, mappings); + createdIndex = index; + + // create the pipeline if one has been supplied + if (pipelineId !== undefined) { + const success = await createPipeline(pipelineId, pipeline); + if (success.acknowledged !== true) { + throw success; + } + } + createdPipelineId = pipelineId; + + } else { + createdIndex = index; + createdPipelineId = pipelineId; + } + + let failures = []; + if (data.length) { + const resp = await indexData(index, createdPipelineId, data); + if (resp.success === false) { + if (resp.ingestError) { + // all docs failed, abort + throw resp; + } else { + // some docs failed. + // still report success but with a list of failures + failures = (resp.failures || []); + } + } + } + + return { + success: true, + id, + index: createdIndex, + pipelineId: createdPipelineId, + docCount, + failures, + }; + } catch (error) { + return { + success: false, + id, + index: createdIndex, + pipelineId: createdPipelineId, + error: (error.error !== undefined) ? error.error : error, + docCount, + ingestError: error.ingestError, + failures: (error.failures || []) + }; + } + } + + async function createIndex(index, settings, mappings) { + const body = { + mappings: { + _meta: { + created_by: INDEX_META_DATA_CREATED_BY + }, + properties: mappings + } + }; + + if (settings && Object.keys(settings).length) { + body.settings = settings; + } + + await callWithRequest('indices.create', { index, body }); + } + + async function indexData(index, pipelineId, data) { + try { + const body = []; + for (let i = 0; i < data.length; i++) { + body.push({ index: {} }); + body.push(data[i]); + } + + const settings = { index, body }; + if (pipelineId !== undefined) { + settings.pipeline = pipelineId; + } + + const resp = await callWithRequest('bulk', settings); + if (resp.errors) { + throw resp; + } else { + return { + success: true, + docs: data.length, + failures: [], + }; + } + } catch (error) { + + let failures = []; + let ingestError = false; + if (error.errors !== undefined && Array.isArray(error.items)) { + // an expected error where some or all of the bulk request + // docs have failed to be ingested. + failures = getFailures(error.items, data); + } else { + // some other error has happened. + ingestError = true; + } + + return { + success: false, + error, + docCount: data.length, + failures, + ingestError, + }; + } + + } + + async function createPipeline(id, pipeline) { + return await callWithRequest('ingest.putPipeline', { id, body: pipeline }); + } + + function getFailures(items, data) { + const failures = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.index && item.index.error) { + failures.push({ + item: i, + reason: item.index.error.reason, + doc: data[i], + }); + } + } + return failures; + } + + return { + importData, + }; +} diff --git a/x-pack/plugins/code/server/lsp/http_request_emitter.ts b/x-pack/plugins/file_upload/server/models/import_data/index.js similarity index 72% rename from x-pack/plugins/code/server/lsp/http_request_emitter.ts rename to x-pack/plugins/file_upload/server/models/import_data/index.js index de4589aeaa9e1..e91b658c4358b 100644 --- a/x-pack/plugins/code/server/lsp/http_request_emitter.ts +++ b/x-pack/plugins/file_upload/server/models/import_data/index.js @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import events from 'events'; -export class HttpRequestEmitter extends events.EventEmitter {} +export { importDataProvider } from './import_data'; diff --git a/x-pack/plugins/file_upload/server/routes/file_upload.js b/x-pack/plugins/file_upload/server/routes/file_upload.js new file mode 100644 index 0000000000000..ac07d80962bdc --- /dev/null +++ b/x-pack/plugins/file_upload/server/routes/file_upload.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { callWithRequestFactory } from '../client/call_with_request_factory'; +import { wrapError } from '../client/errors'; +import { importDataProvider } from '../models/import_data'; +import { MAX_BYTES } from '../../common/constants/file_import'; +import { updateTelemetry } from '../telemetry/telemetry'; + + +function importData({ + callWithRequest, id, index, settings, mappings, ingestPipeline, data +}) { + const { importData: importDataFunc } = importDataProvider(callWithRequest); + return importDataFunc(id, index, settings, mappings, ingestPipeline, data); +} + +export function fileUploadRoutes(server, commonRouteConfig) { + + server.route({ + method: 'POST', + path: '/api/fileupload/import', + async handler(request) { + + // `id` being `undefined` tells us that this is a new import due to create a new index. + // follow-up import calls to just add additional data will include the `id` of the created + // index, we'll ignore those and don't increment the counter. + const { id } = request.query; + if (id === undefined) { + await updateTelemetry({ server, ...request.payload }); + } + + const callWithRequest = callWithRequestFactory(server, request); + return importData({ callWithRequest, id, ...request.payload }) + .catch(wrapError); + }, + config: { + ...commonRouteConfig, + payload: { maxBytes: MAX_BYTES }, + } + }); +} diff --git a/x-pack/plugins/file_upload/server/telemetry/index.ts b/x-pack/plugins/file_upload/server/telemetry/index.ts new file mode 100644 index 0000000000000..d05f7cc63c896 --- /dev/null +++ b/x-pack/plugins/file_upload/server/telemetry/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './telemetry'; +export { makeUsageCollector } from './make_usage_collector'; diff --git a/x-pack/plugins/file_upload/server/telemetry/make_usage_collector.ts b/x-pack/plugins/file_upload/server/telemetry/make_usage_collector.ts new file mode 100644 index 0000000000000..f589280d8cf3a --- /dev/null +++ b/x-pack/plugins/file_upload/server/telemetry/make_usage_collector.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import { getTelemetry, initTelemetry, Telemetry } from './telemetry'; + +// TODO this type should be defined by the platform +interface KibanaHapiServer extends Server { + usage: { + collectorSet: { + makeUsageCollector: any; + register: any; + }; + }; +} + +export function makeUsageCollector(server: KibanaHapiServer): void { + const fileUploadUsageCollector = server.usage.collectorSet.makeUsageCollector({ + type: 'fileUploadTelemetry', + isReady: () => true, + fetch: async (): Promise => (await getTelemetry(server)) || initTelemetry(), + }); + server.usage.collectorSet.register(fileUploadUsageCollector); +} diff --git a/x-pack/plugins/file_upload/server/telemetry/telemetry.test.ts b/x-pack/plugins/file_upload/server/telemetry/telemetry.test.ts new file mode 100644 index 0000000000000..5017c9cb41f08 --- /dev/null +++ b/x-pack/plugins/file_upload/server/telemetry/telemetry.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getTelemetry, updateTelemetry } from './telemetry'; + +const internalRepository = () => ({ + get: jest.fn(() => null), + create: jest.fn(() => ({ attributes: 'test' })), + update: jest.fn(() => ({ attributes: 'test' })), +}); +const server: any = { + savedObjects: { + getSavedObjectsRepository: jest.fn(() => internalRepository()), + }, + plugins: { + elasticsearch: { + getCluster: jest.fn(() => ({ callWithInternalUser })), + }, + }, +}; +const callWithInternalUser = jest.fn(); + +function mockInit(getVal: any = { attributes: {} }): any { + return { + ...internalRepository(), + get: jest.fn(() => getVal), + }; +} + +describe('file upload plugin telemetry', () => { + describe('getTelemetry', () => { + it('should get existing telemetry', async () => { + const internalRepo = mockInit(); + await getTelemetry(server, internalRepo); + expect(internalRepo.update.mock.calls.length).toBe(0); + expect(internalRepo.get.mock.calls.length).toBe(1); + expect(internalRepo.create.mock.calls.length).toBe(0); + }); + }); + + describe('updateTelemetry', () => { + it('should update existing telemetry', async () => { + const internalRepo = mockInit({ + attributes: { + filesUploadedTotalCount: 2, + }, + }); + await updateTelemetry({ server, internalRepo }); + expect(internalRepo.update.mock.calls.length).toBe(1); + expect(internalRepo.get.mock.calls.length).toBe(1); + expect(internalRepo.create.mock.calls.length).toBe(0); + }); + }); +}); diff --git a/x-pack/plugins/file_upload/server/telemetry/telemetry.ts b/x-pack/plugins/file_upload/server/telemetry/telemetry.ts new file mode 100644 index 0000000000000..b43e2a1b33a29 --- /dev/null +++ b/x-pack/plugins/file_upload/server/telemetry/telemetry.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import _ from 'lodash'; +import { callWithInternalUserFactory } from '../client/call_with_internal_user_factory'; + +export const TELEMETRY_DOC_ID = 'file-upload-telemetry'; + +export interface Telemetry { + filesUploadedTotalCount: number; +} + +export interface TelemetrySavedObject { + attributes: Telemetry; +} + +export function getInternalRepository(server: Server): any { + const { getSavedObjectsRepository } = server.savedObjects; + const callWithInternalUser = callWithInternalUserFactory(server); + return getSavedObjectsRepository(callWithInternalUser); +} + +export function initTelemetry(): Telemetry { + return { + filesUploadedTotalCount: 0, + }; +} + +export async function getTelemetry(server: Server, internalRepo?: object): Promise { + const internalRepository = internalRepo || getInternalRepository(server); + let telemetrySavedObject; + + try { + telemetrySavedObject = await internalRepository.get(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID); + } catch (e) { + // Fail silently + } + + return telemetrySavedObject ? telemetrySavedObject.attributes : null; +} + +export async function updateTelemetry({ + server, + internalRepo, +}: { + server: any; + internalRepo?: any; +}) { + const internalRepository = internalRepo || getInternalRepository(server); + let telemetry = await getTelemetry(server, internalRepository); + // Create if doesn't exist + if (!telemetry || _.isEmpty(telemetry)) { + const newTelemetrySavedObject = await internalRepository.create( + TELEMETRY_DOC_ID, + initTelemetry(), + { id: TELEMETRY_DOC_ID } + ); + telemetry = newTelemetrySavedObject.attributes; + } + + await internalRepository.update(TELEMETRY_DOC_ID, TELEMETRY_DOC_ID, incrementCounts(telemetry)); +} + +export function incrementCounts({ filesUploadedTotalCount }: { filesUploadedTotalCount: number }) { + return { + // TODO: get telemetry for app, total file counts, file type + filesUploadedTotalCount: filesUploadedTotalCount + 1, + }; +} diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js index b8901cc8e4d1a..ffb7212d59dd4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js @@ -192,7 +192,7 @@ describe('ilm summary extension', () => { test('should render null when index has no index lifecycle policy', () => { const extension = ilmSummaryExtension(indexWithoutLifecyclePolicy); const rendered = mountWithIntl(extension); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should return extension when index has lifecycle policy', () => { const extension = ilmSummaryExtension(indexWithLifecyclePolicy); diff --git a/x-pack/plugins/infra/public/components/auto_sizer.tsx b/x-pack/plugins/infra/public/components/auto_sizer.tsx index 674a54338dcf1..1d6798aacebe5 100644 --- a/x-pack/plugins/infra/public/components/auto_sizer.tsx +++ b/x-pack/plugins/infra/public/components/auto_sizer.tsx @@ -19,7 +19,7 @@ interface Measurements { } interface AutoSizerProps { - detectAnyWindowResize?: boolean; + detectAnyWindowResize?: boolean | 'height' | 'width'; bounds?: boolean; content?: boolean; onResize?: (size: Measurements) => void; @@ -37,6 +37,7 @@ export class AutoSizer extends React.PureComponent window.innerWidth - ) { - const gap = this.windowWidth - window.innerWidth; - boundsMeasurement.width = boundsMeasurement.width - gap; + if (this.props.detectAnyWindowResize && boundsMeasurement) { + if ( + boundsMeasurement.width && + this.windowWidth !== -1 && + this.windowWidth > window.innerWidth + ) { + const gap = this.windowWidth - window.innerWidth; + boundsMeasurement.width = boundsMeasurement.width - gap; + } + if ( + boundsMeasurement.height && + this.windowHeight !== -1 && + this.windowHeight > window.innerHeight + ) { + const gap = this.windowHeight - window.innerHeight; + boundsMeasurement.height = boundsMeasurement.height - gap; + } } this.windowWidth = window.innerWidth; + this.windowHeight = window.innerHeight; const contentRect = content && entry ? entry.contentRect : null; const contentMeasurement = contentRect && entry @@ -111,7 +121,6 @@ export class AutoSizer extends React.PureComponent { - window.setTimeout(() => { - this.measure(null); - }, 0); - }; + private updateMeasurement = () => + requestAnimationFrame(() => { + const { detectAnyWindowResize } = this.props; + if (!detectAnyWindowResize) return; + switch (detectAnyWindowResize) { + case 'height': + if (this.windowHeight !== window.innerHeight) { + this.measure(null); + } + break; + case 'width': + if (this.windowWidth !== window.innerWidth) { + this.measure(null); + } + break; + default: + this.measure(null); + } + }); private storeRef = (element: HTMLElement | null) => { if (this.element && this.resizeObserver) { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index cb58cc723e917..f1405d294b9d9 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -155,8 +155,8 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent< columnWidths={columnWidths} showColumnConfiguration={showColumnConfiguration} /> - - {({ measureRef, content: { width = 0, height = 0 } }) => ( + + {({ measureRef, bounds: { height = 0 }, content: { width = 0 } }) => ( void; } -const dateFormatter = timeFormatter(niceTimeFormatByDay(1)); - export const MetricsExplorerChart = injectI18n( ({ source, @@ -56,6 +53,10 @@ export const MetricsExplorerChart = injectI18n( const handleTimeChange = (from: number, to: number) => { onTimeChange(moment(from).toISOString(), moment(to).toISOString()); }; + const dateFormatter = useCallback( + niceTimeFormatter([first(series.rows).timestamp, last(series.rows).timestamp]), + [series, series.rows] + ); const yAxisFormater = useCallback(createFormatterForMetric(first(metrics)), [options]); return ( @@ -103,7 +104,7 @@ export const MetricsExplorerChart = injectI18n( tickFormat={dateFormatter} /> - + ) : options.metrics.length > 0 ? ( diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts index f7ff6fad45584..92d91a2043927 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createTSVBLink } from './create_tsvb_link'; +import { createTSVBLink, createFilterFromOptions } from './create_tsvb_link'; import { source, options, timeRange } from '../../../utils/fixtures/metrics_explorer'; import uuid from 'uuid'; import { OutputBuffer } from 'uuid/interfaces'; @@ -19,7 +19,7 @@ describe('createTSVBLink()', () => { it('should just work', () => { const link = createTSVBLink(source, options, series, timeRange); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:'host.name: example-01',id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" ); }); it('should work with rates', () => { @@ -31,14 +31,14 @@ describe('createTSVBLink()', () => { }; const link = createTSVBLink(source, customOptions, series, timeRange); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:'host.name: example-01',id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:bytes,id:test-id,label:'rate(system.network.out.bytes)',line_width:2,metrics:!((field:system.network.out.bytes,id:test-id,type:max),(field:test-id,id:test-id,type:derivative,unit:'1s'),(field:test-id,id:test-id,type:positive_only)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}}/s)),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:bytes,id:test-id,label:'rate(system.network.out.bytes)',line_width:2,metrics:!((field:system.network.out.bytes,id:test-id,type:max),(field:test-id,id:test-id,type:derivative,unit:'1s'),(field:test-id,id:test-id,type:positive_only)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}}/s)),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" ); }); it('should work with time range', () => { const customTimeRange = { ...timeRange, from: 'now-10m', to: 'now' }; const link = createTSVBLink(source, options, series, customTimeRange); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-10m,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:'host.name: example-01',id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-10m,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'metricbeat-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metricbeat-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))" ); }); it('should work with source', () => { @@ -49,7 +49,7 @@ describe('createTSVBLink()', () => { }; const link = createTSVBLink(customSource, options, series, timeRange); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:'host.name: example-01',id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" ); }); it('should work with filterQuery', () => { @@ -61,7 +61,16 @@ describe('createTSVBLink()', () => { const customOptions = { ...options, filterQuery: 'system.network.name:lo*' }; const link = createTSVBLink(customSource, customOptions, series, timeRange); expect(link).toBe( - "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:'system.network.name:lo* AND host.name: example-01',id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" + "../app/kibana#/visualize/create?type=metrics&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))&_a=(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:%233185FC,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))" ); }); + + test('createFilterFromOptions()', () => { + const customOptions = { ...options, groupBy: 'host.name' }; + const customSeries = { ...series, id: 'test"foo' }; + expect(createFilterFromOptions(customOptions, customSeries)).toEqual({ + language: 'kuery', + query: 'host.name : "test\\"foo"', + }); + }); }); diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts index bf9b460162978..9053002795d45 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts @@ -79,7 +79,7 @@ const mapMetricToSeries = (metric: MetricsExplorerOptionsMetric) => { }; }; -const createFilterFromOptions = ( +export const createFilterFromOptions = ( options: MetricsExplorerOptions, series: MetricsExplorerSeries ) => { @@ -88,9 +88,10 @@ const createFilterFromOptions = ( filters.push(options.filterQuery); } if (options.groupBy) { - filters.push(`${options.groupBy}: ${series.id}`); + const id = series.id.replace('"', '\\"'); + filters.push(`${options.groupBy} : "${id}"`); } - return filters.join(' AND '); + return { language: 'kuery', query: filters.join(' and ') }; }; export const createTSVBLink = ( diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts b/x-pack/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts new file mode 100644 index 0000000000000..7be2e54d47d75 --- /dev/null +++ b/x-pack/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import chrome from 'ui/chrome'; +import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; + +export function getChartTheme(): Theme { + const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); + return isDarkMode ? DARK_THEME : LIGHT_THEME; +} diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/line_series.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/line_series.tsx index 3c0924c4ca2cf..6e29d94362798 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/line_series.tsx +++ b/x-pack/plugins/infra/public/components/metrics_explorer/line_series.tsx @@ -12,7 +12,6 @@ import { DataSeriesColorsValues, CustomSeriesColorsMap, } from '@elastic/charts'; -import '@elastic/charts/dist/style.css'; import { MetricsExplorerSeries } from '../../../server/routes/metrics_explorer/types'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { createMetricLabel } from './helpers/create_metric_label'; diff --git a/x-pack/plugins/infra/public/components/waffle/node.tsx b/x-pack/plugins/infra/public/components/waffle/node.tsx index 067934f0b2da1..d10c8b6223774 100644 --- a/x-pack/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/components/waffle/node.tsx @@ -170,24 +170,23 @@ const ValueInner = euiStyled.button` } `; -const Value = euiStyled('div')` - font-weight: bold; - font-size: 0.9em; +const SquareTextContent = euiStyled('div')` text-align: center; width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; flex: 1 0 auto; - line-height: 1.2em; color: ${props => readableColor(props.color)}; `; -const Label = euiStyled('div')` - text-overflow: ellipsis; +const Value = euiStyled(SquareTextContent)` + font-weight: bold; + font-size: 0.9em; + line-height: 1.2em; +`; + +const Label = euiStyled(SquareTextContent)` font-size: 0.7em; margin-bottom: 0.7em; - text-align: center; - width: 100%; - flex: 1 0 auto; - white-space: nowrap; - overflow: hidden; - color: ${props => readableColor(props.color)}; `; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx index f0e5ec54610bf..11b5799fe7286 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx @@ -55,7 +55,7 @@ export const InfrastructurePage = injectI18n(({ match, intl }: InfrastructurePag { title: intl.formatMessage({ id: 'xpack.infra.homePage.metricsExplorerTabTitle', - defaultMessage: 'Metrics explorer', + defaultMessage: 'Metrics Explorer', }), path: `${match.path}/metrics-explorer`, }, diff --git a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx index 040e29e06a09c..538319adb6f96 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx @@ -48,7 +48,7 @@ export const MetricsExplorerPage = injectI18n( intl.formatMessage( { id: 'xpack.infra.infrastructureMetricsExplorerPage.documentTitle', - defaultMessage: '{previousTitle} | Metrics explorer', + defaultMessage: '{previousTitle} | Metrics Explorer', }, { previousTitle, diff --git a/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx index cdc76def51e24..bb52a2929eb20 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_logs_content.tsx @@ -111,8 +111,8 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { )} - - {({ measureRef, content: { width = 0, height = 0 } }) => { + + {({ measureRef, bounds: { height = 0 }, content: { width = 0 } }) => { return ( diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 6bfecdee4b8dc..2fbc099015ca8 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -17,6 +17,7 @@ import { MetricsExplorerWrappedRequest, } from '../types'; import { createMetricModel } from './create_metrics_model'; +import { JsonObject } from '../../../../common/typed_json'; export const populateSeriesWithTSVBData = ( req: InfraFrameworkRequest, @@ -33,7 +34,22 @@ export const populateSeriesWithTSVBData = ( } // Set the filter for the group by or match everything - const filters = options.groupBy ? [{ match: { [options.groupBy]: series.id } }] : []; + const filters: JsonObject[] = options.groupBy + ? [{ match: { [options.groupBy]: series.id } }] + : []; + if (options.filterQuery) { + try { + const filterQuery = JSON.parse(options.filterQuery); + filters.push(filterQuery); + } catch (error) { + filters.push({ + query_string: { + query: options.filterQuery, + analyze_wildcard: true, + }, + }); + } + } const timerange = { min: options.timerange.from, max: options.timerange.to }; // Create the TSVB model based on the request options diff --git a/x-pack/plugins/license_management/__jest__/request_trial_extension.test.js b/x-pack/plugins/license_management/__jest__/request_trial_extension.test.js index 74e1084fb0e78..e990bd48317d1 100644 --- a/x-pack/plugins/license_management/__jest__/request_trial_extension.test.js +++ b/x-pack/plugins/license_management/__jest__/request_trial_extension.test.js @@ -19,8 +19,7 @@ describe('RequestTrialExtension component', () => { }, RequestTrialExtension ); - const html = rendered.html(); - expect(html).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should display when license is active and trial has been used', () => { const rendered = getComponent( @@ -46,8 +45,7 @@ describe('RequestTrialExtension component', () => { }, RequestTrialExtension ); - const html = rendered.html(); - expect(html).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should display when license is not active and trial has been used', () => { const rendered = getComponent( @@ -87,7 +85,6 @@ describe('RequestTrialExtension component', () => { }, RequestTrialExtension ); - const html = rendered.html(); - expect(html).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/license_management/__jest__/revert_to_basic.test.js b/x-pack/plugins/license_management/__jest__/revert_to_basic.test.js index 33a5727974109..76057ff95d41f 100644 --- a/x-pack/plugins/license_management/__jest__/revert_to_basic.test.js +++ b/x-pack/plugins/license_management/__jest__/revert_to_basic.test.js @@ -45,7 +45,7 @@ describe('RevertToBasic component', () => { }, RevertToBasic ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should not display for active gold license', () => { const rendered = getComponent( @@ -54,7 +54,7 @@ describe('RevertToBasic component', () => { }, RevertToBasic ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should not display for active platinum license', () => { const rendered = getComponent( @@ -63,6 +63,6 @@ describe('RevertToBasic component', () => { }, RevertToBasic ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/license_management/__jest__/start_trial.test.js b/x-pack/plugins/license_management/__jest__/start_trial.test.js index e9c3721a8f56b..928c7df70d0b7 100644 --- a/x-pack/plugins/license_management/__jest__/start_trial.test.js +++ b/x-pack/plugins/license_management/__jest__/start_trial.test.js @@ -38,7 +38,7 @@ describe('StartTrial component when trial is allowed', () => { }, StartTrial ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should not display for active platinum license', () => { const rendered = getComponent( @@ -48,7 +48,7 @@ describe('StartTrial component when trial is allowed', () => { }, StartTrial ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should display for expired platinum license', () => { const rendered = getComponent( @@ -71,7 +71,7 @@ describe('StartTrial component when trial is not available', () => { }, StartTrial ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should not display for gold license', () => { const rendered = getComponent( @@ -81,7 +81,7 @@ describe('StartTrial component when trial is not available', () => { }, StartTrial ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should not display for platinum license', () => { const rendered = getComponent( @@ -91,7 +91,7 @@ describe('StartTrial component when trial is not available', () => { }, StartTrial ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); test('should not display for trial license', () => { @@ -102,6 +102,6 @@ describe('StartTrial component when trial is not available', () => { }, StartTrial ); - expect(rendered.html()).toBeNull(); + expect(rendered.isEmptyRender()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/license_management/common/constants/index.js b/x-pack/plugins/license_management/common/constants/index.js index 58b6a9ab6992f..c115fb7b69c0e 100644 --- a/x-pack/plugins/license_management/common/constants/index.js +++ b/x-pack/plugins/license_management/common/constants/index.js @@ -7,3 +7,4 @@ export { PLUGIN } from './plugin'; export { BASE_PATH } from './base_path'; export { EXTERNAL_LINKS } from './external_links'; +export { APP_PERMISSION } from './permissions'; diff --git a/x-pack/plugins/license_management/common/constants/permissions.js b/x-pack/plugins/license_management/common/constants/permissions.js new file mode 100644 index 0000000000000..dea9530952464 --- /dev/null +++ b/x-pack/plugins/license_management/common/constants/permissions.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const APP_PERMISSION = 'cluster:manage'; diff --git a/x-pack/plugins/license_management/index.js b/x-pack/plugins/license_management/index.js index 86cb102cbd044..1c69b5d96aad6 100644 --- a/x-pack/plugins/license_management/index.js +++ b/x-pack/plugins/license_management/index.js @@ -6,7 +6,12 @@ import { resolve } from 'path'; import { PLUGIN } from './common/constants'; -import { registerLicenseRoute, registerStartTrialRoutes, registerStartBasicRoute } from './server/routes/api/license/'; +import { + registerLicenseRoute, + registerStartTrialRoutes, + registerStartBasicRoute, + registerPermissionsRoute +} from './server/routes/api/license/'; import { createRouter } from '../../server/lib/create_router'; export function licenseManagement(kibana) { @@ -27,6 +32,7 @@ export function licenseManagement(kibana) { registerLicenseRoute(router, xpackInfo); registerStartTrialRoutes(router, xpackInfo); registerStartBasicRoute(router, xpackInfo); + registerPermissionsRoute(router, xpackInfo); } }); } diff --git a/x-pack/plugins/license_management/public/app.container.js b/x-pack/plugins/license_management/public/app.container.js new file mode 100644 index 0000000000000..eac12710aeedc --- /dev/null +++ b/x-pack/plugins/license_management/public/app.container.js @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import { App as PresentationComponent } from './app'; +import { getPermission, isPermissionsLoading, getPermissionsError } from './store/reducers/licenseManagement'; +import { loadPermissions } from './store/actions/permissions'; + +const mapStateToProps = state => { + return { + hasPermission: getPermission(state), + permissionsLoading: isPermissionsLoading(state), + permissionsError: getPermissionsError(state), + }; +}; + +const mapDispatchToProps = { + loadPermissions, +}; + +export const App = withRouter(connect(mapStateToProps, mapDispatchToProps)( + PresentationComponent +)); diff --git a/x-pack/plugins/license_management/public/app.js b/x-pack/plugins/license_management/public/app.js index 28f68aa05bc2c..ff3a2d6196d76 100644 --- a/x-pack/plugins/license_management/public/app.js +++ b/x-pack/plugins/license_management/public/app.js @@ -4,17 +4,90 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LicenseDashboard, UploadLicense } from './sections/'; import { Switch, Route } from 'react-router-dom'; -import { BASE_PATH } from '../common/constants'; -import { EuiPageBody } from '@elastic/eui'; +import { BASE_PATH, APP_PERMISSION } from '../common/constants'; +import { EuiPageBody, EuiEmptyPrompt, EuiText, EuiLoadingSpinner, EuiCallOut } from '@elastic/eui'; -export default () => ( - - - - - - -); +export class App extends Component { + componentDidMount() { + const { loadPermissions } = this.props; + loadPermissions(); + } + + render() { + const { hasPermission, permissionsLoading, permissionsError } = this.props; + + if (permissionsLoading) { + return ( + } + body={( + + + + )} + data-test-subj="sectionLoading" + /> + ); + } + + if (permissionsError) { + return ( + } + color="danger" + iconType="alert" + > + {permissionsError.data && permissionsError.data.message ?
{permissionsError.data.message}
: null} +
+ ); + } + + if (!hasPermission) { + return ( + + + + + } + body={ +

+ {APP_PERMISSION} + }} + /> +

+ } + /> +
+ ); + } + + return ( + + + + + + + ); + } +} diff --git a/x-pack/plugins/license_management/public/lib/es.js b/x-pack/plugins/license_management/public/lib/es.js index 886c44421e666..24dbf659ae657 100644 --- a/x-pack/plugins/license_management/public/lib/es.js +++ b/x-pack/plugins/license_management/public/lib/es.js @@ -57,3 +57,15 @@ export function canStartTrial() { return $.ajax(options); } +export function getPermissions() { + const options = { + url: chrome.addBasePath('/api/license/permissions'), + contentType: 'application/json', + cache: false, + crossDomain: true, + type: 'POST', + }; + + return $.ajax(options); +} + diff --git a/x-pack/plugins/license_management/public/register_route.js b/x-pack/plugins/license_management/public/register_route.js index 6cc90639f3122..793cbad4c6341 100644 --- a/x-pack/plugins/license_management/public/register_route.js +++ b/x-pack/plugins/license_management/public/register_route.js @@ -12,7 +12,7 @@ import { setTelemetryOptInService, setTelemetryEnabled, setHttpClient, Telemetry import { I18nContext } from 'ui/i18n'; import chrome from 'ui/chrome'; -import App from './app'; +import { App } from './app.container'; import { BASE_PATH } from '../common/constants/base_path'; import routes from 'ui/routes'; diff --git a/x-pack/plugins/license_management/public/store/actions/permissions.js b/x-pack/plugins/license_management/public/store/actions/permissions.js new file mode 100644 index 0000000000000..b9316c0a958c4 --- /dev/null +++ b/x-pack/plugins/license_management/public/store/actions/permissions.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { getPermissions } from '../../lib/es'; + +export const permissionsLoading = createAction( + 'LICENSE_MANAGEMENT_PERMISSIONS_LOADING' +); + +export const permissionsSuccess = createAction( + 'LICENSE_MANAGEMENT_PERMISSIONS_SUCCESS' +); + +export const permissionsError = createAction( + 'LICENSE_MANAGEMENT_PERMISSIONS_ERROR' +); + +export const loadPermissions = () => async dispatch => { + dispatch(permissionsLoading(true)); + try { + const permissions = await getPermissions(); + dispatch(permissionsLoading(false)); + dispatch(permissionsSuccess(permissions.hasPermission)); + } catch (e) { + dispatch(permissionsLoading(false)); + dispatch(permissionsError(e)); + } +}; diff --git a/x-pack/plugins/license_management/public/store/reducers/licenseManagement.js b/x-pack/plugins/license_management/public/store/reducers/licenseManagement.js index 4b79b330da2c1..2fa01f1807982 100644 --- a/x-pack/plugins/license_management/public/store/reducers/licenseManagement.js +++ b/x-pack/plugins/license_management/public/store/reducers/licenseManagement.js @@ -10,6 +10,7 @@ import { uploadStatus } from './upload_status'; import { startBasicStatus } from './start_basic_license_status'; import { uploadErrorMessage } from './upload_error_message'; import { trialStatus } from './trial_status'; +import { permissions } from './permissions'; import moment from 'moment-timezone'; export const WARNING_THRESHOLD_IN_DAYS = 25; @@ -19,9 +20,23 @@ export const licenseManagement = combineReducers({ uploadStatus, uploadErrorMessage, trialStatus, - startBasicStatus + startBasicStatus, + permissions, }); +export const getPermission = state => { + return state.permissions.hasPermission; +}; + +export const isPermissionsLoading = state => { + return state.permissions.loading; +}; + + +export const getPermissionsError = state => { + return state.permissions.error; +}; + export const getLicense = state => { return state.license; }; diff --git a/x-pack/plugins/license_management/public/store/reducers/permissions.js b/x-pack/plugins/license_management/public/store/reducers/permissions.js new file mode 100644 index 0000000000000..a1d2361338e49 --- /dev/null +++ b/x-pack/plugins/license_management/public/store/reducers/permissions.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions } from 'redux-actions'; + +import { permissionsSuccess, permissionsError, permissionsLoading } from '../actions/permissions'; + +export const permissions = handleActions({ + [permissionsLoading](state, { payload }) { + return { + loading: payload, + }; + }, + [permissionsSuccess](state, { payload }) { + return { + hasPermission: payload, + }; + }, + [permissionsError](state, { payload }) { + return { + error: payload, + }; + }, +}, {}); diff --git a/x-pack/plugins/license_management/server/lib/permissions.js b/x-pack/plugins/license_management/server/lib/permissions.js new file mode 100644 index 0000000000000..11d25a9c2dc24 --- /dev/null +++ b/x-pack/plugins/license_management/server/lib/permissions.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { wrapCustomError } from '../../../../server/lib/create_router/error_wrappers'; + +export async function getPermissions(req, xpackInfo) { + if (!xpackInfo) { + // xpackInfo is updated via poll, so it may not be available until polling has begun. + // In this rare situation, tell the client the service is temporarily unavailable. + throw wrapCustomError(new Error('Security info unavailable'), 503); + } + + const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); + if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { + // If security isn't enabled, let the user use license management + return { + hasPermission: true, + }; + } + + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); + const options = { + method: 'POST', + path: '/_security/user/_has_privileges', + body: { + cluster: ['manage'], // License management requires "manage" cluster privileges + } + }; + + try { + const response = await callWithRequest(req, 'transport.request', options); + return { + hasPermission: response.cluster.manage, + }; + } catch (error) { + return error.body; + } + +} diff --git a/x-pack/plugins/license_management/server/routes/api/license/index.js b/x-pack/plugins/license_management/server/routes/api/license/index.js index 59e3b9eab298d..1667ab1dd9427 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/index.js +++ b/x-pack/plugins/license_management/server/routes/api/license/index.js @@ -7,3 +7,4 @@ export { registerLicenseRoute } from './register_license_route'; export { registerStartBasicRoute } from './register_start_basic_route'; export { registerStartTrialRoutes } from './register_start_trial_routes'; +export { registerPermissionsRoute } from './register_permissions_route'; diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.js b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.js new file mode 100644 index 0000000000000..d8fd4e5abd8f2 --- /dev/null +++ b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getPermissions } from '../../../lib/permissions'; + +export function registerPermissionsRoute(router, xpackInfo) { + router.post('/permissions', (request) => { + return getPermissions(request, xpackInfo); + }); +} diff --git a/x-pack/plugins/maps/common/constants.js b/x-pack/plugins/maps/common/constants.js index bc67e46042aee..d30dbc7abb6e6 100644 --- a/x-pack/plugins/maps/common/constants.js +++ b/x-pack/plugins/maps/common/constants.js @@ -24,6 +24,8 @@ export const ES_GEO_GRID = 'ES_GEO_GRID'; export const ES_SEARCH = 'ES_SEARCH'; export const SOURCE_DATA_ID_ORIGIN = 'source'; +export const GEOJSON_FILE = 'GEOJSON_FILE'; + export const DECIMAL_DEGREES_PRECISION = 5; // meters precision export const ZOOM_PRECISION = 2; export const DEFAULT_ES_DOC_LIMIT = 2048; diff --git a/x-pack/plugins/maps/public/angular/map_controller.js b/x-pack/plugins/maps/public/angular/map_controller.js index fbbe0c5e4b4c9..44fc9e33d2bcf 100644 --- a/x-pack/plugins/maps/public/angular/map_controller.js +++ b/x-pack/plugins/maps/public/angular/map_controller.js @@ -39,7 +39,7 @@ import { import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; import { getInspectorAdapters } from '../store/non_serializable_instances'; import { Inspector } from 'ui/inspector'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import { indexPatternService } from '../kibana_services'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; @@ -55,7 +55,7 @@ const REACT_ANCHOR_DOM_ELEMENT_ID = 'react-maps-root'; const app = uiModules.get('app/maps', []); -app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage, AppState, globalState, Private) => { +app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage, AppState, globalState) => { const savedMap = $route.current.locals.map; let unsubscribe; @@ -235,7 +235,6 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage async function doSave(saveOptions) { await store.dispatch(clearTransientLayerStateAndCloseFlyout()); savedMap.syncWithStore(store.getState()); - const docTitle = Private(DocTitleProvider); let id; try { diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss index 299ad3585c95e..78438c8742c2e 100644 --- a/x-pack/plugins/maps/public/components/_index.scss +++ b/x-pack/plugins/maps/public/components/_index.scss @@ -1,5 +1,5 @@ @import './gis_map/gis_map'; -@import './layer_addpanel/layer_addpanel'; +@import './layer_addpanel/source_select/index'; @import './layer_panel/index'; @import './widget_overlay/index'; @import './toolbar_overlay/index'; diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/flyout_footer/index.js b/x-pack/plugins/maps/public/components/layer_addpanel/flyout_footer/index.js new file mode 100644 index 0000000000000..2b4832290e636 --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/flyout_footer/index.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import { connect } from 'react-redux'; +import { FlyoutFooter } from './view'; +import { getSelectedLayer } from '../../../selectors/map_selectors'; +import { + clearTransientLayerStateAndCloseFlyout, +} from '../../../actions/store_actions'; + +function mapStateToProps(state = {}) { + const selectedLayer = getSelectedLayer(state); + return { + hasLayerSelected: !!selectedLayer, + isLoading: selectedLayer && selectedLayer.isLayerLoading(), + }; +} + +function mapDispatchToProps(dispatch) { + return { + closeFlyout: () => dispatch(clearTransientLayerStateAndCloseFlyout()), + }; +} + +const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(FlyoutFooter); +export { connectedFlyOut as FlyoutFooter }; + diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/flyout_footer/view.js b/x-pack/plugins/maps/public/components/layer_addpanel/flyout_footer/view.js new file mode 100644 index 0000000000000..d2fab0b862880 --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/flyout_footer/view.js @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const FlyoutFooter = ({ + onClick, showNextButton, disableNextButton, nextButtonText, closeFlyout, + hasLayerSelected, isLoading +}) => { + + const nextButton = showNextButton + ? ( + + {nextButtonText} + + ) + : null; + + return ( + + + + + + + + + {nextButton} + + + + ); +}; diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/import_editor/index.js b/x-pack/plugins/maps/public/components/layer_addpanel/import_editor/index.js new file mode 100644 index 0000000000000..b8b155a9596fb --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/import_editor/index.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import { connect } from 'react-redux'; +import { ImportEditor } from './view'; +import { getInspectorAdapters } from '../../../store/non_serializable_instances'; +import { INDEXING_STAGE, updateIndexingStage, getIndexingStage } from '../../../store/ui'; + +function mapStateToProps(state = {}) { + return { + inspectorAdapters: getInspectorAdapters(state), + isIndexingTriggered: getIndexingStage(state) === INDEXING_STAGE.TRIGGERED, + }; +} + +const mapDispatchToProps = { + onIndexReady: indexReady => indexReady + ? updateIndexingStage(INDEXING_STAGE.READY) + : updateIndexingStage(null), + importSuccessHandler: () => updateIndexingStage(INDEXING_STAGE.SUCCESS), + importErrorHandler: () => updateIndexingStage(INDEXING_STAGE.ERROR), +}; + +const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(ImportEditor); +export { connectedFlyOut as ImportEditor }; + diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/import_editor/view.js b/x-pack/plugins/maps/public/components/layer_addpanel/import_editor/view.js new file mode 100644 index 0000000000000..f8ae193024c94 --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/import_editor/view.js @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { GeojsonFileSource } from '../../../shared/layers/sources/client_file_source'; +import { + EuiSpacer, + EuiPanel, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const ImportEditor = ({ + clearSource, isIndexingTriggered, ...props +}) => { + const editorProperties = getEditorProperties({ isIndexingTriggered, ...props }); + const editor = GeojsonFileSource.renderEditor(editorProperties); + return ( + + { + isIndexingTriggered + ? null + : ( + + + + + + + ) + } + + {editor} + + + ); +}; + +function getEditorProperties({ + inspectorAdapters, onRemove, viewLayer, + isIndexingTriggered, onIndexReady, importSuccessHandler, importErrorHandler +}) { + return { + onPreviewSource: viewLayer, + inspectorAdapters, + onRemove, + importSuccessHandler, + importErrorHandler, + isIndexingTriggered, + addAndViewSource: viewLayer, + onIndexReady, + }; +} diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/index.js b/x-pack/plugins/maps/public/components/layer_addpanel/index.js index 1ce809a0b4581..1c8955002edd1 100644 --- a/x-pack/plugins/maps/public/components/layer_addpanel/index.js +++ b/x-pack/plugins/maps/public/components/layer_addpanel/index.js @@ -6,37 +6,32 @@ import { connect } from 'react-redux'; import { AddLayerPanel } from './view'; -import { getFlyoutDisplay, updateFlyout, FLYOUT_STATE } from '../../store/ui'; -import { getSelectedLayer, getMapColors } from '../../selectors/map_selectors'; +import { getFlyoutDisplay, updateFlyout, FLYOUT_STATE, updateIndexingStage, + getIndexingStage, INDEXING_STAGE } from '../../store/ui'; +import { getMapColors } from '../../selectors/map_selectors'; import { getInspectorAdapters } from '../../store/non_serializable_instances'; import { - clearTransientLayerStateAndCloseFlyout, setTransientLayer, addLayer, setSelectedLayer, - removeTransientLayer + removeTransientLayer, } from '../../actions/store_actions'; function mapStateToProps(state = {}) { - const selectedLayer = getSelectedLayer(state); + const indexingStage = getIndexingStage(state); return { inspectorAdapters: getInspectorAdapters(state), flyoutVisible: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, - hasLayerSelected: !!selectedLayer, - isLoading: selectedLayer && selectedLayer.isLayerLoading(), mapColors: getMapColors(state), + isIndexingTriggered: indexingStage === INDEXING_STAGE.TRIGGERED, + isIndexingSuccess: indexingStage === INDEXING_STAGE.SUCCESS, + isIndexingReady: indexingStage === INDEXING_STAGE.READY, }; } function mapDispatchToProps(dispatch) { return { - closeFlyout: () => { - dispatch(clearTransientLayerStateAndCloseFlyout()); - }, - previewLayer: async (layer) => { - //this removal always needs to happen prior to adding the new layer - //many source editors allow users to modify the settings in the add-source wizard - //this triggers a new request for preview. Any existing transient layers need to be cleared before the new one can be added. + viewLayer: async layer => { await dispatch(setSelectedLayer(null)); await dispatch(removeTransientLayer()); dispatch(addLayer(layer.toLayerDescriptor())); @@ -51,6 +46,8 @@ function mapDispatchToProps(dispatch) { dispatch(setTransientLayer(null)); dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); }, + setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)), + resetIndexing: () => dispatch(updateIndexingStage(null)), }; } diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/source_editor/index.js b/x-pack/plugins/maps/public/components/layer_addpanel/source_editor/index.js new file mode 100644 index 0000000000000..1802d84b00e9d --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/source_editor/index.js @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + + +import { connect } from 'react-redux'; +import { SourceEditor } from './view'; +import { getInspectorAdapters } from '../../../store/non_serializable_instances'; + +function mapStateToProps(state = {}) { + return { + inspectorAdapters: getInspectorAdapters(state), + }; +} + +const connectedFlyOut = connect(mapStateToProps)(SourceEditor); +export { connectedFlyOut as SourceEditor }; + diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/source_editor/view.js b/x-pack/plugins/maps/public/components/layer_addpanel/source_editor/view.js new file mode 100644 index 0000000000000..99fb7a76ff165 --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/source_editor/view.js @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { ALL_SOURCES } from '../../../shared/layers/sources/all_sources'; +import { + EuiSpacer, + EuiPanel, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const SourceEditor = ({ + clearSource, sourceType, isIndexingTriggered, inspectorAdapters, previewLayer +}) => { + const editorProperties = { + onPreviewSource: previewLayer, + inspectorAdapters, + }; + const Source = ALL_SOURCES.find(Source => { + return Source.type === sourceType; + }); + if (!Source) { + throw new Error(`Unexpected source type: ${sourceType}`); + } + const editor = Source.renderEditor(editorProperties); + return ( + + { + isIndexingTriggered + ? null + : ( + + + + + + + ) + } + + {editor} + + + ); +}; + diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/source_select/_index.scss b/x-pack/plugins/maps/public/components/layer_addpanel/source_select/_index.scss new file mode 100644 index 0000000000000..7fe1396fcca16 --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/source_select/_index.scss @@ -0,0 +1 @@ +@import './source_select'; diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/_layer_addpanel.scss b/x-pack/plugins/maps/public/components/layer_addpanel/source_select/_source_select.scss similarity index 100% rename from x-pack/plugins/maps/public/components/layer_addpanel/_layer_addpanel.scss rename to x-pack/plugins/maps/public/components/layer_addpanel/source_select/_source_select.scss diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/source_select/source_select.js b/x-pack/plugins/maps/public/components/layer_addpanel/source_select/source_select.js new file mode 100644 index 0000000000000..3c83e9b4e9b62 --- /dev/null +++ b/x-pack/plugins/maps/public/components/layer_addpanel/source_select/source_select.js @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { ALL_SOURCES } from '../../../shared/layers/sources/all_sources'; +import { + EuiTitle, + EuiSpacer, + EuiCard, + EuiIcon, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import _ from 'lodash'; + +export function SourceSelect({ + updateSourceSelection +}) { + + const sourceCards = ALL_SOURCES.map(Source => { + const icon = Source.icon + ? + : null; + + return ( + + + updateSourceSelection( + { type: Source.type, isIndexingSource: Source.isIndexingSource }) + } + description={Source.description} + layout="horizontal" + data-test-subj={_.camelCase(Source.title)} + /> + + ); + }); + + return ( + + +

+ +

+
+ {sourceCards} +
+ ); +} diff --git a/x-pack/plugins/maps/public/components/layer_addpanel/view.js b/x-pack/plugins/maps/public/components/layer_addpanel/view.js index 6af578ee5860f..a935f7b8c5158 100644 --- a/x-pack/plugins/maps/public/components/layer_addpanel/view.js +++ b/x-pack/plugins/maps/public/components/layer_addpanel/view.js @@ -4,167 +4,145 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; -import { ALL_SOURCES } from '../../shared/layers/sources/all_sources'; +import React, { Component } from 'react'; +import { SourceSelect } from './source_select/source_select'; +import { FlyoutFooter } from './flyout_footer'; +import { SourceEditor } from './source_editor'; +import { ImportEditor } from './import_editor'; import { - EuiButton, - EuiButtonEmpty, EuiFlexGroup, - EuiFlexItem, EuiTitle, - EuiPanel, - EuiSpacer, - EuiCard, - EuiIcon, EuiFlyoutHeader, - EuiFlyoutFooter, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; export class AddLayerPanel extends Component { state = { sourceType: null, - layer: null + layer: null, + importView: false, + layerImportAddReady: false, } - _previewLayer = (source) => { + componentDidUpdate() { + if (!this.state.layerImportAddReady && this.props.isIndexingSuccess) { + this.setState({ layerImportAddReady: true }); + } + } + + _getPanelDescription() { + const { sourceType, importView, layerImportAddReady } = this.state; + let panelDescription; + if (!sourceType) { + panelDescription = i18n.translate('xpack.maps.addLayerPanel.selectSource', + { defaultMessage: 'Select source' }); + } else if (layerImportAddReady || !importView) { + panelDescription = i18n.translate('xpack.maps.addLayerPanel.addLayer', + { defaultMessage: 'Add layer' }); + } else { + panelDescription = i18n.translate('xpack.maps.addLayerPanel.importFile', + { defaultMessage: 'Import file' }); + } + return panelDescription; + } + + _viewLayer = async source => { if (!source) { this.setState({ layer: null }); this.props.removeTransientLayer(); return; } - const layerOptions = this.state.layer ? { style: this.state.layer.getCurrentStyle().getDescriptor() } : {}; - this.setState({ - layer: source.createDefaultLayer(layerOptions, this.props.mapColors) - }, - () => this.props.previewLayer(this.state.layer)); + const newLayer = source.createDefaultLayer(layerOptions, this.props.mapColors); + this.setState({ layer: newLayer }, () => + this.props.viewLayer(this.state.layer)); }; - _clearSource = () => { + _clearLayerData = ({ keepSourceType = false }) => { this.setState({ layer: null, - sourceType: null + ...( + !keepSourceType + ? { sourceType: null, importView: false } + : {} + ), }); this.props.removeTransientLayer(); } - _onSourceTypeChange = (sourceType) => { - this.setState({ sourceType }); + _onSourceSelectionChange = ({ type, isIndexingSource }) => { + this.setState({ sourceType: type, importView: isIndexingSource }); } - _renderNextBtn() { - if (!this.state.sourceType) { - return null; + _layerAddHandler = () => { + const { isIndexingTriggered, setIndexingTriggered, selectLayerAndAdd, + resetIndexing } = this.props; + const layerSource = this.state.layer.getSource(); + const boolIndexLayer = layerSource.shouldBeIndexed(); + this.setState({ layer: null }); + if (boolIndexLayer && !isIndexingTriggered) { + setIndexingTriggered(); + } else { + selectLayerAndAdd(); + if (this.state.importView) { + this.setState({ + layerImportAddReady: false, + }); + resetIndexing(); + } } - - const { hasLayerSelected, isLoading, selectLayerAndAdd } = this.props; - return ( - { - this.setState({ layer: null }); - selectLayerAndAdd(); - }} - fill - > - - - ); } - _renderSourceCards() { - return ALL_SOURCES.map(Source => { - const icon = Source.icon - ? - : null; + _renderAddLayerPanel() { + const { sourceType, importView } = this.state; + if (!sourceType) { return ( - - - this._onSourceTypeChange(Source.type)} - description={Source.description} - layout="horizontal" - data-test-subj={_.camelCase(Source.title)} - /> - + ); - }); - } - - _renderSourceSelect() { + } + if (importView) { + return ( + this._clearLayerData({ keepSourceType: true })} + /> + ); + } return ( - - -

- -

-
- {this._renderSourceCards()} -
+ ); } - _renderSourceEditor() { - const editorProperties = { - onPreviewSource: this._previewLayer, - inspectorAdapters: this.props.inspectorAdapters, - }; + _renderFooter(buttonDescription) { + const { importView, layer } = this.state; + const { isIndexingReady, isIndexingSuccess } = this.props; - const Source = ALL_SOURCES.find((Source) => { - return Source.type === this.state.sourceType; - }); - if (!Source) { - throw new Error(`Unexepected source type: ${this.state.sourceType}`); - } + const buttonEnabled = importView + ? isIndexingReady || isIndexingSuccess + : !!layer; return ( - - - - - - - {Source.renderEditor(editorProperties)} - - + ); } - _renderAddLayerForm() { - if (!this.state.sourceType) { - return this._renderSourceSelect(); - } - - return this._renderSourceEditor(); - } - _renderFlyout() { + const panelDescription = this._getPanelDescription(); + return (

- + {panelDescription}

- {this._renderAddLayerForm()} + { this._renderAddLayerPanel() }
- - - - - - - - - - {this._renderNextBtn()} - - - + { this._renderFooter(panelDescription) }
); } diff --git a/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap b/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap index c9c15552bcbc9..36f2ef7465fad 100644 --- a/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap +++ b/x-pack/plugins/maps/public/components/map/__snapshots__/feature_tooltip.test.js.snap @@ -18,6 +18,7 @@ exports[`FeatureTooltip should show both filter buttons and close button 1`] = ` ); @@ -151,6 +152,7 @@ export class FeatureTooltip extends React.Component { aria-label={i18n.translate('xpack.maps.tooltip.closeAriaLabel', { defaultMessage: 'Close tooltip' })} + data-test-subj="mapTooltipCloseButton" /> ); diff --git a/x-pack/plugins/maps/public/components/map/mb/view.js b/x-pack/plugins/maps/public/components/map/mb/view.js index e9add84440320..b1191e59c1107 100644 --- a/x-pack/plugins/maps/public/components/map/mb/view.js +++ b/x-pack/plugins/maps/public/components/map/mb/view.js @@ -508,7 +508,14 @@ export class MBMapContainer extends React.Component { }; render() { - return (
); + return ( +
+ ); } } diff --git a/x-pack/plugins/maps/public/components/toolbar_overlay/_index.scss b/x-pack/plugins/maps/public/components/toolbar_overlay/_index.scss index da662d99d080d..d969e27504c8d 100644 --- a/x-pack/plugins/maps/public/components/toolbar_overlay/_index.scss +++ b/x-pack/plugins/maps/public/components/toolbar_overlay/_index.scss @@ -1,6 +1,6 @@ .mapToolbarOverlay { position: absolute; - top: ($euiSizeM * 2) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin + top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin left: $euiSizeM; z-index: 2; // Sit on top of mapbox controls shadow } @@ -8,6 +8,7 @@ .mapToolbarOverlay__button { @include size($euiSizeXL); background-color: $euiColorEmptyShade !important; + pointer-events: all; &:enabled, &:enabled:hover, diff --git a/x-pack/plugins/maps/public/components/toolbar_overlay/index.js b/x-pack/plugins/maps/public/components/toolbar_overlay/index.js index 7c5b24a611511..c5d3cf04de26a 100644 --- a/x-pack/plugins/maps/public/components/toolbar_overlay/index.js +++ b/x-pack/plugins/maps/public/components/toolbar_overlay/index.js @@ -5,27 +5,16 @@ */ import { connect } from 'react-redux'; -import { ToolbarOverlay } from './view'; -import { getDrawState, getUniqueIndexPatternIds } from '../../selectors/map_selectors'; +import { ToolbarOverlay } from './toolbar_overlay'; +import { getUniqueIndexPatternIds } from '../../selectors/map_selectors'; import { getIsFilterable } from '../../store/ui'; -import { updateDrawState } from '../../actions/store_actions'; - function mapStateToProps(state = {}) { return { isFilterable: getIsFilterable(state), - drawState: getDrawState(state), uniqueIndexPatternIds: getUniqueIndexPatternIds(state) }; } -function mapDispatchToProps(dispatch) { - return { - initiateDraw: (options) => { - dispatch(updateDrawState(options)); - } - }; -} - -const connectedToolbarOverlay = connect(mapStateToProps, mapDispatchToProps)(ToolbarOverlay); +const connectedToolbarOverlay = connect(mapStateToProps, null)(ToolbarOverlay); export { connectedToolbarOverlay as ToolbarOverlay }; diff --git a/x-pack/plugins/maps/public/components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/components/toolbar_overlay/set_view_control/index.js new file mode 100644 index 0000000000000..87eabc81936b4 --- /dev/null +++ b/x-pack/plugins/maps/public/components/toolbar_overlay/set_view_control/index.js @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { SetViewControl } from './set_view_control'; +import { setGotoWithCenter } from '../../../actions/store_actions'; +import { getMapZoom, getMapCenter } from '../../../selectors/map_selectors'; +import { + getIsSetViewOpen, + closeSetView, + openSetView, +} from '../../../store/ui'; + +function mapStateToProps(state = {}) { + return { + isSetViewOpen: getIsSetViewOpen(state), + zoom: getMapZoom(state), + center: getMapCenter(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + onSubmit: ({ lat, lon, zoom }) => { + dispatch(closeSetView()); + dispatch(setGotoWithCenter({ lat, lon, zoom })); + }, + closeSetView: () => { + dispatch(closeSetView()); + }, + openSetView: () => { + dispatch(openSetView()); + } + }; +} + +const connectedSetViewControl = connect(mapStateToProps, mapDispatchToProps)(SetViewControl); +export { connectedSetViewControl as SetViewControl }; diff --git a/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/set_view.js b/x-pack/plugins/maps/public/components/toolbar_overlay/set_view_control/set_view_control.js similarity index 61% rename from x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/set_view.js rename to x-pack/plugins/maps/public/components/toolbar_overlay/set_view_control/set_view_control.js index 0faf5d86b2f07..362d366fd445e 100644 --- a/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/set_view.js +++ b/x-pack/plugins/maps/public/components/toolbar_overlay/set_view_control/set_view_control.js @@ -4,23 +4,49 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { EuiForm, EuiFormRow, EuiButton, EuiFieldNumber, + EuiButtonIcon, + EuiPopover, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -export class SetView extends React.Component { +function getViewString(lat, lon, zoom) { + return `${lat},${lon},${zoom}`; +} + +export class SetViewControl extends Component { + + state = {} + + static getDerivedStateFromProps(nextProps, prevState) { + const nextView = getViewString(nextProps.center.lat, nextProps.center.lon, nextProps.zoom); + if (nextView !== prevState.prevView) { + return { + lat: nextProps.center.lat, + lon: nextProps.center.lon, + zoom: nextProps.zoom, + prevView: nextView, + }; + } - state = { - lat: this.props.center.lat, - lon: this.props.center.lon, - zoom: this.props.zoom, + return null; } + _togglePopover = () => { + if (this.props.isSetViewOpen) { + this.props.closeSetView(); + return; + } + + this.props.openSetView(); + }; + _onLatChange = evt => { this._onChange('lat', evt); }; @@ -63,7 +89,7 @@ export class SetView extends React.Component { }; } - onSubmit = () => { + _onSubmit = () => { const { lat, lon, @@ -72,7 +98,7 @@ export class SetView extends React.Component { this.props.onSubmit({ lat, lon, zoom }); } - render() { + _renderSetViewForm() { const { isInvalid: isLatInvalid, component: latFormRow } = this._renderNumberFormRow({ value: this.state.lat, min: -90, @@ -113,7 +139,7 @@ export class SetView extends React.Component { Go @@ -123,13 +149,43 @@ export class SetView extends React.Component { ); } + + render() { + return ( + + )} + isOpen={this.props.isSetViewOpen} + closePopover={this.props.closeSetView} + > + {this._renderSetViewForm()} + + ); + } } -SetView.propTypes = { +SetViewControl.propTypes = { + isSetViewOpen: PropTypes.bool.isRequired, zoom: PropTypes.number.isRequired, center: PropTypes.shape({ lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired }), onSubmit: PropTypes.func.isRequired, + closeSetView: PropTypes.func.isRequired, + openSetView: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/maps/public/components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/components/toolbar_overlay/toolbar_overlay.js new file mode 100644 index 0000000000000..970c06c600a2e --- /dev/null +++ b/x-pack/plugins/maps/public/components/toolbar_overlay/toolbar_overlay.js @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { getIndexPatternsFromIds } from '../../index_pattern_util'; +import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; +import { SetViewControl } from './set_view_control'; +import { ToolsControl } from './tools_control'; + +export class ToolbarOverlay extends React.Component { + + state = { + prevUniqueIndexPatternIds: [], + uniqueIndexPatternsAndGeoFields: [], + }; + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + if (this.props.isFilterable) { + const nextUniqueIndexPatternIds = _.get(this.props, 'uniqueIndexPatternIds', []); + this._loadUniqueIndexPatternAndFieldCombos(nextUniqueIndexPatternIds); + } + } + + _loadUniqueIndexPatternAndFieldCombos = async (nextUniqueIndexPatternIds) => { + if (_.isEqual(nextUniqueIndexPatternIds, this.state.prevUniqueIndexPatternIds)) { + // all ready loaded index pattern ids + return; + } + + this.setState({ + prevUniqueIndexPatternIds: nextUniqueIndexPatternIds, + }); + + const uniqueIndexPatternsAndGeoFields = []; + try { + const indexPatterns = await getIndexPatternsFromIds(nextUniqueIndexPatternIds); + indexPatterns.forEach((indexPattern) => { + indexPattern.fields.forEach(field => { + if (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) { + uniqueIndexPatternsAndGeoFields.push({ + geoField: field.name, + geoFieldType: field.type, + indexPatternTitle: indexPattern.title, + indexPatternId: indexPattern.id + }); + } + }); + }); + } catch(e) { + // swallow errors. + // the Layer-TOC will indicate which layers are disfunctional on a per-layer basis + } + + if (!this._isMounted) { + return; + } + + this.setState({ uniqueIndexPatternsAndGeoFields }); + } + + _renderToolsControl() { + const { uniqueIndexPatternsAndGeoFields } = this.state; + if ( + !this.props.isFilterable || + !uniqueIndexPatternsAndGeoFields.length + ) { + return null; + } + + return ( + + + + ); + } + + render() { + return ( + + + + + + + {this._renderToolsControl()} + + + ); + } +} diff --git a/x-pack/plugins/maps/public/components/toolbar_overlay/tools_control/index.js b/x-pack/plugins/maps/public/components/toolbar_overlay/tools_control/index.js new file mode 100644 index 0000000000000..8cc61a9b384b9 --- /dev/null +++ b/x-pack/plugins/maps/public/components/toolbar_overlay/tools_control/index.js @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { ToolsControl } from './tools_control'; +import { getDrawState } from '../../../selectors/map_selectors'; +import { updateDrawState } from '../../../actions/store_actions'; + +function mapStateToProps(state = {}) { + return { + drawState: getDrawState(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + initiateDraw: (options) => { + dispatch(updateDrawState(options)); + } + }; +} + +const connectedToolsControl = connect(mapStateToProps, mapDispatchToProps)(ToolsControl); +export { connectedToolsControl as ToolsControl }; diff --git a/x-pack/plugins/maps/public/components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/components/toolbar_overlay/tools_control/tools_control.js new file mode 100644 index 0000000000000..ac22f68061182 --- /dev/null +++ b/x-pack/plugins/maps/public/components/toolbar_overlay/tools_control/tools_control.js @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { + EuiButtonIcon, + EuiPopover, + EuiContextMenu, + EuiSelectable, + EuiHighlight, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DRAW_TYPE } from '../../../actions/store_actions'; + +const RESET_STATE = { + isPopoverOpen: false, + drawType: null +}; + +export class ToolsControl extends Component { + + state = { + ...RESET_STATE + }; + + _togglePopover = () => { + this.setState(prevState => ({ + ...RESET_STATE, + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState(RESET_STATE); + }; + + _onIndexPatternSelection = (options) => { + const selection = options.find((option) => option.checked); + this._initiateDraw( + this.state.drawType, + selection.value + ); + }; + + _initiateDraw = (drawType, indexContext) => { + this.props.initiateDraw({ + drawType, + ...indexContext + }); + this._closePopover(); + }; + + _selectPolygonDrawType = () => { + this.setState({ drawType: DRAW_TYPE.POLYGON }); + } + + _selectBoundsDrawType = () => { + this.setState({ drawType: DRAW_TYPE.BOUNDS }); + } + + _getDrawPanels() { + + const needsIndexPatternSelectionPanel = this.props.uniqueIndexPatternsAndGeoFields.length > 1; + + const drawPolygonAction = { + name: i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { + defaultMessage: 'Draw shape to filter data', + }), + onClick: needsIndexPatternSelectionPanel + ? this._selectPolygonDrawType + : () => { + this._initiateDraw(DRAW_TYPE.POLYGON, this.props.uniqueIndexPatternsAndGeoFields[0]); + }, + panel: needsIndexPatternSelectionPanel + ? this._getIndexPatternSelectionPanel(1) + : undefined + }; + + const drawBoundsAction = { + name: i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabel', { + defaultMessage: 'Draw bounds to filter data', + }), + onClick: needsIndexPatternSelectionPanel + ? this._selectBoundsDrawType + : () => { + this._initiateDraw(DRAW_TYPE.BOUNDS, this.props.uniqueIndexPatternsAndGeoFields[0]); + }, + panel: needsIndexPatternSelectionPanel + ? this._getIndexPatternSelectionPanel(2) + : undefined + }; + + return flattenPanelTree({ + id: 0, + title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', { + defaultMessage: 'Tools', + }), + items: [drawPolygonAction, drawBoundsAction] + }); + } + + _getIndexPatternSelectionPanel(id) { + const options = this.props.uniqueIndexPatternsAndGeoFields.map((indexPatternAndGeoField) => { + return { + label: `${indexPatternAndGeoField.indexPatternTitle} : ${indexPatternAndGeoField.geoField}`, + value: indexPatternAndGeoField + }; + }); + + const renderGeoField = (option, searchValue) => { + return ( + + + + {option.value.indexPatternTitle} + + +
+ + {option.value.geoField} + +
+ ); + }; + + const indexPatternSelection = ( + + {(list, search) => ( +
+ {search} + {list} +
+ )} +
+ ); + + return { + id: id, + title: i18n.translate('xpack.maps.toolbarOverlay.geofield.toolbarTitle', { + defaultMessage: 'Select geo field', + }), + content: indexPatternSelection + }; + } + + _renderToolsButton() { + return ( + + ); + } + + render() { + return ( + + + + ); + } +} + +function flattenPanelTree(tree, array = []) { + array.push(tree); + + if (tree.items) { + tree.items.forEach(item => { + if (item.panel) { + flattenPanelTree(item.panel, array); + item.panel = item.panel.id; + } + }); + } + + return array; +} diff --git a/x-pack/plugins/maps/public/components/toolbar_overlay/view.js b/x-pack/plugins/maps/public/components/toolbar_overlay/view.js deleted file mode 100644 index 9b8c81a828fef..0000000000000 --- a/x-pack/plugins/maps/public/components/toolbar_overlay/view.js +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiPopover, - EuiContextMenu, - EuiSelectable, - EuiHighlight, - EuiTextColor, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { getIndexPatternsFromIds } from '../../index_pattern_util'; -import _ from 'lodash'; -import { DRAW_TYPE } from '../../actions/store_actions'; -import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; - -const RESET_STATE = { - isPopoverOpen: false, - drawType: null -}; - -export class ToolbarOverlay extends React.Component { - - - state = { - isPopoverOpen: false, - uniqueIndexPatternsAndGeoFields: [], - drawType: null - }; - - _toggleToolbar = () => { - if (!this._isMounted) { - return; - } - this.setState(prevState => ({ - isPopoverOpen: !prevState.isPopoverOpen, - drawType: null - })); - }; - - _closePopover = () => { - if (!this._isMounted) { - return; - } - this.setState(RESET_STATE); - }; - - _onIndexPatternSelection = (options) => { - if (!this._isMounted) { - return; - } - - const selection = options.find((option) => option.checked); - const drawType = this.state.drawType; - this.setState(RESET_STATE, () => { - if (drawType) { - this.props.initiateDraw({ drawType: drawType, ...selection.value }); - } - }); - }; - - _activateDrawForFirstIndexPattern = (drawType) => { - if (!this._isMounted) { - return; - } - const indexPatternAndGeofield = this.state.uniqueIndexPatternsAndGeoFields[0]; - this.setState(RESET_STATE, () => { - this.props.initiateDraw({ drawType: drawType, ...indexPatternAndGeofield }); - }); - }; - - componentDidMount() { - this._isMounted = true; - } - - componentWillUnmount() { - this._isMounted = false; - } - - async _getuniqueIndexPatternAndFieldCombos() { - try { - const indexPatterns = await getIndexPatternsFromIds(this.props.uniqueIndexPatternIds); - const uniqueIndexPatternsAndGeofields = []; - indexPatterns.forEach((indexPattern) => { - indexPattern.fields.forEach(field => { - if (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - uniqueIndexPatternsAndGeofields.push({ - geoField: field.name, - geoFieldType: field.type, - indexPatternTitle: indexPattern.title, - indexPatternId: indexPattern.id - }); - } - }); - }); - if (this._isMounted && !_.isEqual(this.state.uniqueIndexPatternsAndGeoFields, uniqueIndexPatternsAndGeofields)) { - this.setState({ - uniqueIndexPatternsAndGeoFields: uniqueIndexPatternsAndGeofields - }); - } - } catch(e) { - // swallow errors. - // the Layer-TOC will indicate which layers are disfunctional on a per-layer basis - return []; - } - } - - componentDidUpdate() { - this._getuniqueIndexPatternAndFieldCombos(); - } - - _getDrawActionsPanel() { - - const drawPolygonAction = { - name: i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { - defaultMessage: 'Draw shape to filter data', - }), - }; - - const drawBoundsAction = { - name: i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLabel', { - defaultMessage: 'Draw bounds to filter data', - }), - }; - - if (this.state.uniqueIndexPatternsAndGeoFields.length === 1) { - drawPolygonAction.onClick = () => this._activateDrawForFirstIndexPattern(DRAW_TYPE.POLYGON); - drawBoundsAction.onClick = () => this._activateDrawForFirstIndexPattern(DRAW_TYPE.BOUNDS); - } else { - drawPolygonAction.panel = this._getIndexPatternSelectionPanel(1); - drawPolygonAction.onClick = () => { - this.setState({ drawType: DRAW_TYPE.POLYGON }); - }; - drawBoundsAction.panel = this._getIndexPatternSelectionPanel(2); - drawBoundsAction.onClick = () => { - this.setState({ drawType: DRAW_TYPE.BOUNDS }); - }; - } - - return flattenPanelTree({ - id: 0, - title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', { - defaultMessage: 'Tools', - }), - items: [drawPolygonAction, drawBoundsAction] - }); - } - - _getIndexPatternSelectionPanel(id) { - const options = this.state.uniqueIndexPatternsAndGeoFields.map((indexPatternAndGeoField) => { - return { - label: `${indexPatternAndGeoField.indexPatternTitle} : ${indexPatternAndGeoField.geoField}`, - value: indexPatternAndGeoField - }; - }); - - const renderGeoField = (option, searchValue) => { - return ( - - - - {option.value.indexPatternTitle} - - -
- - {option.value.geoField} - -
- ); - }; - - const indexPatternSelection = ( - - {(list, search) => ( -
- {search} - {list} -
- )} -
- ); - - return { - id: id, - title: i18n.translate('xpack.maps.toolbarOverlay.geofield.toolbarTitle', { - defaultMessage: 'Select geo field', - }), - content: indexPatternSelection - }; - } - - _renderToolbarButton() { - return ( - - ); - } - - render() { - - if ( - !this.props.isFilterable || - !this.state.uniqueIndexPatternsAndGeoFields.length - ) { - return null; - } - - return ( - - - - - - - - ); - } -} - - -function flattenPanelTree(tree, array = []) { - array.push(tree); - - if (tree.items) { - tree.items.forEach(item => { - if (item.panel) { - flattenPanelTree(item.panel, array); - item.panel = item.panel.id; - } - }); - } - - return array; -} diff --git a/x-pack/plugins/maps/public/components/widget_overlay/layer_control/_layer_control.scss b/x-pack/plugins/maps/public/components/widget_overlay/layer_control/_layer_control.scss index 2d63cd62ed528..83e693ba58d7b 100644 --- a/x-pack/plugins/maps/public/components/widget_overlay/layer_control/_layer_control.scss +++ b/x-pack/plugins/maps/public/components/widget_overlay/layer_control/_layer_control.scss @@ -11,8 +11,7 @@ } .mapLayerControl__addLayerButton, -.mapLayerControl__openLayerTOCButton, -.mapViewControl__gotoButton { +.mapLayerControl__openLayerTOCButton { pointer-events: all; &:enabled, @@ -23,8 +22,7 @@ } .mapLayerControl__openLayerTOCButton, -.mapLayerControl__closeLayerTOCButton, -.mapViewControl__gotoButton { +.mapLayerControl__closeLayerTOCButton { @include size($euiSizeXL); background-color: $euiColorEmptyShade !important; // During all states } diff --git a/x-pack/plugins/maps/public/components/widget_overlay/view_control/_view_control.scss b/x-pack/plugins/maps/public/components/widget_overlay/view_control/_view_control.scss index d77bc7ba20d21..c5de44e8742f0 100644 --- a/x-pack/plugins/maps/public/components/widget_overlay/view_control/_view_control.scss +++ b/x-pack/plugins/maps/public/components/widget_overlay/view_control/_view_control.scss @@ -1,16 +1,5 @@ -/** - * 1. The overlay captures mouse events even if it's empty space. To counter-act this, - * we remove all pointer events from the overlay then add them back on the - * individual widgets. - */ - .mapViewControl__coordinates { @include mapOverlayIsTextOnly; justify-content: center; pointer-events: none; } - -.mapViewControl__gotoButton { - min-width: 0; - pointer-events: all; /* 1 */ -} diff --git a/x-pack/plugins/maps/public/components/widget_overlay/view_control/index.js b/x-pack/plugins/maps/public/components/widget_overlay/view_control/index.js index a3bf1e5d924e3..4212b2e067dc7 100644 --- a/x-pack/plugins/maps/public/components/widget_overlay/view_control/index.js +++ b/x-pack/plugins/maps/public/components/widget_overlay/view_control/index.js @@ -6,30 +6,14 @@ import { connect } from 'react-redux'; import { ViewControl } from './view_control'; -import { getMouseCoordinates } from '../../../selectors/map_selectors'; -import { - getIsSetViewOpen, - closeSetView, - openSetView, -} from '../../../store/ui'; +import { getMouseCoordinates, getMapZoom } from '../../../selectors/map_selectors'; function mapStateToProps(state = {}) { return { - isSetViewOpen: getIsSetViewOpen(state), mouseCoordinates: getMouseCoordinates(state), + zoom: getMapZoom(state), }; } -function mapDispatchToProps(dispatch) { - return { - closeSetView: () => { - dispatch(closeSetView()); - }, - openSetView: () => { - dispatch(openSetView()); - } - }; -} - -const connectedViewControl = connect(mapStateToProps, mapDispatchToProps)(ViewControl); +const connectedViewControl = connect(mapStateToProps, null)(ViewControl); export { connectedViewControl as ViewControl }; diff --git a/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/index.js b/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/index.js deleted file mode 100644 index 66fd61ba2eade..0000000000000 --- a/x-pack/plugins/maps/public/components/widget_overlay/view_control/set_view/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { connect } from 'react-redux'; -import { SetView } from './set_view'; -import { setGotoWithCenter } from '../../../../actions/store_actions'; -import { getMapZoom, getMapCenter } from '../../../../selectors/map_selectors'; -import { closeSetView } from '../../../../store/ui'; - -function mapStateToProps(state = {}) { - return { - zoom: getMapZoom(state), - center: getMapCenter(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - onSubmit: ({ lat, lon, zoom }) => { - dispatch(closeSetView()); - dispatch(setGotoWithCenter({ lat, lon, zoom })); - } - }; -} - -const connectedSetView = connect(mapStateToProps, mapDispatchToProps, null, { withRef: true })(SetView); -export { connectedSetView as SetView }; diff --git a/x-pack/plugins/maps/public/components/widget_overlay/view_control/view_control.js b/x-pack/plugins/maps/public/components/widget_overlay/view_control/view_control.js index 225beb282e5af..c1fbf4172e724 100644 --- a/x-pack/plugins/maps/public/components/widget_overlay/view_control/view_control.js +++ b/x-pack/plugins/maps/public/components/widget_overlay/view_control/view_control.js @@ -6,95 +6,39 @@ import _ from 'lodash'; import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiPopover, - EuiText, -} from '@elastic/eui'; -import { SetView } from './set_view'; +import { EuiText } from '@elastic/eui'; import { DECIMAL_DEGREES_PRECISION } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -export function ViewControl({ isSetViewOpen, closeSetView, openSetView, mouseCoordinates }) { - const toggleSetViewVisibility = () => { - if (isSetViewOpen) { - closeSetView(); - return; - } - - openSetView(); - }; - const setView = ( - - )} - isOpen={isSetViewOpen} - closePopover={closeSetView} - > - - - ); - - function renderMouseCoordinates() { - const lat = mouseCoordinates - ? _.round(mouseCoordinates.lat, DECIMAL_DEGREES_PRECISION) - : ''; - const lon = mouseCoordinates - ? _.round(mouseCoordinates.lon, DECIMAL_DEGREES_PRECISION) - : ''; - return ( -
- - - - - {lat},{' '} - - - {lon} - - -
- ); +export function ViewControl({ mouseCoordinates, zoom }) { + if (!mouseCoordinates) { + return null; } return ( - - - {mouseCoordinates && renderMouseCoordinates()} - - - - {setView} - - +
+ + + + + {_.round(mouseCoordinates.lat, DECIMAL_DEGREES_PRECISION)},{' '} + + + {_.round(mouseCoordinates.lon, DECIMAL_DEGREES_PRECISION)},{' '} + + + {zoom} + + +
); } diff --git a/x-pack/plugins/maps/public/index.js b/x-pack/plugins/maps/public/index.js index 9096d677b8c3e..0e664c37e9a56 100644 --- a/x-pack/plugins/maps/public/index.js +++ b/x-pack/plugins/maps/public/index.js @@ -21,7 +21,7 @@ import chrome from 'ui/chrome'; import routes from 'ui/routes'; import 'ui/kbn_top_nav'; import { uiModules } from 'ui/modules'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; import 'ui/autoload/styles'; import 'ui/autoload/all'; import 'react-vis/dist/style.css'; @@ -103,10 +103,8 @@ routes template: mapTemplate, controller: 'GisMapController', resolve: { - map: function (gisMapSavedObjectLoader, redirectWhenMissing, $route, - Private) { + map: function (gisMapSavedObjectLoader, redirectWhenMissing, $route) { const id = $route.current.params.id; - const docTitle = Private(DocTitleProvider); return gisMapSavedObjectLoader.get(id) .then((savedMap) => { recentlyAccessed.add(savedMap.getFullPath(), savedMap.title, id); diff --git a/x-pack/plugins/maps/public/index_pattern_util.js b/x-pack/plugins/maps/public/index_pattern_util.js index 705371c433a4e..ad72fb7c35bb7 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.js +++ b/x-pack/plugins/maps/public/index_pattern_util.js @@ -6,7 +6,7 @@ import { indexPatternService } from './kibana_services'; -export async function getIndexPatternsFromIds(indexPatternIds) { +export async function getIndexPatternsFromIds(indexPatternIds = []) { const promises = []; indexPatternIds.forEach((id) => { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js index 5a667595b13d9..6c3cf0e5e95fb 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -99,15 +99,6 @@ export const getMapCenter = ({ map }) => map.mapState.center ? export const getMouseCoordinates = ({ map }) => map.mapState.mouseCoordinates; -export const getMapColors = ({ map }) => { - return map.layerList.reduce((accu, layer) => { - // This will evolve as color options are expanded - const color = _.get(layer, 'style.properties.fillColor.options.color'); - if (color) accu.push(color); - return accu; - }, []); -}; - export const getTimeFilters = ({ map }) => map.mapState.timeFilters ? map.mapState.timeFilters : timefilter.getTime(); @@ -167,6 +158,19 @@ export const getSelectedLayer = createSelector( return layerList.find(layer => layer.getId() === selectedLayerId); }); +export const getMapColors = createSelector( + getTransientLayerId, + getLayerListRaw, + (transientLayerId, layerList) => layerList.reduce((accu, layer) => { + if (layer.id === transientLayerId) { + return accu; + } + const color = _.get(layer, 'style.properties.fillColor.options.color'); + if (color) accu.push(color); + return accu; + }, []) +); + export const getSelectedLayerJoinDescriptors = createSelector( getSelectedLayer, (selectedLayer) => { @@ -183,7 +187,7 @@ export const getUniqueIndexPatternIds = createSelector( layerList.forEach(layer => { indexPatternIds.push(...layer.getIndexPatternIds()); }); - return _.uniq(indexPatternIds); + return _.uniq(indexPatternIds).sort(); } ); diff --git a/x-pack/plugins/maps/public/shared/layers/sources/all_sources.js b/x-pack/plugins/maps/public/shared/layers/sources/all_sources.js index 43f4d40ad2b06..d81d218910ccb 100644 --- a/x-pack/plugins/maps/public/shared/layers/sources/all_sources.js +++ b/x-pack/plugins/maps/public/shared/layers/sources/all_sources.js @@ -6,6 +6,7 @@ import { EMSFileSource } from './ems_file_source'; +import { GeojsonFileSource } from './client_file_source'; import { KibanaRegionmapSource } from './kibana_regionmap_source'; import { XYZTMSSource } from './xyz_tms_source'; import { EMSTMSSource } from './ems_tms_source'; @@ -16,6 +17,7 @@ import { ESSearchSource } from './es_search_source'; export const ALL_SOURCES = [ + GeojsonFileSource, ESSearchSource, ESGeoGridSource, EMSFileSource, diff --git a/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/create_client_file_source_editor.js b/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/create_client_file_source_editor.js new file mode 100644 index 0000000000000..de05e10bf42c5 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/create_client_file_source_editor.js @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + +import React from 'react'; +import { JsonUploadAndParse } from '../../../../../../file_upload/public'; + +export function ClientFileCreateSourceEditor({ + previewGeojsonFile, + isIndexingTriggered = false, + onIndexingComplete, + onRemove, + onIndexReady, +}) { + return ( + + ); +} diff --git a/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/geojson_file_source.js new file mode 100644 index 0000000000000..3a0ecf78d9581 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/geojson_file_source.js @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AbstractVectorSource } from '../vector_source'; +import React from 'react'; +import { ES_GEO_FIELD_TYPE, GEOJSON_FILE } from '../../../../../common/constants'; +import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; +import { ESSearchSource } from '../es_search_source'; +import uuid from 'uuid/v4'; +import _ from 'lodash'; + +export class GeojsonFileSource extends AbstractVectorSource { + + static type = GEOJSON_FILE; + static title = 'Upload GeoJSON vector file'; + static description = 'Upload a GeoJSON file and index in Elasticsearch'; + static icon = 'importAction'; + static isIndexingSource = true; + + static createDescriptor(geoJson, name) { + // Wrap feature as feature collection if needed + const featureCollection = (geoJson.type === 'Feature') + ? { + type: 'FeatureCollection', + features: [{ ...geoJson }] + } + : geoJson; + + return { + type: GeojsonFileSource.type, + featureCollection, + name + }; + } + + static viewIndexedData = ( + addAndViewSource, inspectorAdapters, importSuccessHandler, importErrorHandler + ) => { + return (indexResponses = {}) => { + const { indexDataResp, indexPatternResp } = indexResponses; + if (!(indexDataResp && indexDataResp.success) || + !(indexPatternResp && indexPatternResp.success)) { + importErrorHandler(indexResponses); + return; + } + const { fields, id } = indexPatternResp; + const geoFieldArr = fields.filter( + field => Object.values(ES_GEO_FIELD_TYPE).includes(field.type) + ); + const geoField = _.get(geoFieldArr, '[0].name'); + const indexPatternId = id; + if (!indexPatternId || !geoField) { + addAndViewSource(null); + } else { + const source = new ESSearchSource({ + id: uuid(), + indexPatternId, + geoField, + }, inspectorAdapters); + addAndViewSource(source); + importSuccessHandler(indexResponses); + } + }; + }; + + static previewGeojsonFile = (onPreviewSource, inspectorAdapters) => { + return (geojsonFile, name) => { + if (!geojsonFile) { + onPreviewSource(null); + return; + } + const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name); + const source = new GeojsonFileSource(sourceDescriptor, inspectorAdapters); + onPreviewSource(source); + }; + }; + + static renderEditor({ + onPreviewSource, inspectorAdapters, addAndViewSource, isIndexingTriggered, + onRemove, onIndexReady, importSuccessHandler, importErrorHandler + }) { + return ( + + ); + } + + async getGeoJsonWithMeta() { + return { + data: this._descriptor.featureCollection, + meta: {} + }; + } + + async getDisplayName() { + return this._descriptor.name; + } + + canFormatFeatureProperties() { + return true; + } + + shouldBeIndexed() { + return GeojsonFileSource.isIndexingSource; + } + +} diff --git a/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/index.js b/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/index.js new file mode 100644 index 0000000000000..cf0d15dcb747a --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/sources/client_file_source/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { GeojsonFileSource } from './geojson_file_source'; diff --git a/x-pack/plugins/maps/public/shared/layers/sources/source.js b/x-pack/plugins/maps/public/shared/layers/sources/source.js index 625d9e6fa1f4d..f843916cabef4 100644 --- a/x-pack/plugins/maps/public/shared/layers/sources/source.js +++ b/x-pack/plugins/maps/public/shared/layers/sources/source.js @@ -8,6 +8,8 @@ import { copyPersistentState } from '../../../store/util'; export class AbstractSource { + static isIndexingSource = false; + static renderEditor() { throw new Error('Must implement Source.renderEditor'); } @@ -109,6 +111,10 @@ export class AbstractSource { return false; } + shouldBeIndexed() { + return AbstractSource.isIndexingSource; + } + supportsElasticsearchFilters() { return false; } diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/dynamic_orientation_selection.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/dynamic_orientation_selection.js new file mode 100644 index 0000000000000..e4a15ad8fcc65 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/dynamic_orientation_selection.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { dynamicOrientationShape } from '../style_option_shapes'; +import { FieldSelect, fieldShape } from '../field_select'; + +export function DynamicOrientationSelection({ ordinalFields, styleOptions, onChange }) { + const onFieldChange = ({ field }) => { + onChange({ ...styleOptions, field }); + }; + + return ( + + ); +} + +DynamicOrientationSelection.propTypes = { + ordinalFields: PropTypes.arrayOf(fieldShape).isRequired, + styleOptions: dynamicOrientationShape.isRequired, + onChange: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/orientation_editor.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/orientation_editor.js new file mode 100644 index 0000000000000..bfe712d13d3af --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/orientation_editor.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { StaticDynamicStyleRow } from '../../static_dynamic_style_row'; +import { DynamicOrientationSelection } from './dynamic_orientation_selection'; +import { StaticOrientationSelection } from './static_orientation_selection'; +import { i18n } from '@kbn/i18n'; + +export function OrientationEditor(props) { + return ( + + ); +} diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/static_orientation_selection.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/static_orientation_selection.js new file mode 100644 index 0000000000000..b20ec69ff64f2 --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/orientation/static_orientation_selection.js @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { staticOrientationShape } from '../style_option_shapes'; +import { ValidatedRange } from '../../../../../components/validated_range'; + +export function StaticOrientationSelection({ onChange, styleOptions }) { + + const onOrientationChange = (orientation) => { + onChange({ orientation }); + }; + + return ( + + ); +} + +StaticOrientationSelection.propTypes = { + styleOptions: staticOrientationShape.isRequired, + onChange: PropTypes.func.isRequired +}; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/style_option_shapes.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/style_option_shapes.js index dc94f0f03ce98..6d8ba676bb57d 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/style_option_shapes.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/style_option_shapes.js @@ -16,6 +16,14 @@ export const dynamicColorShape = PropTypes.shape({ field: fieldShape, }); +export const staticOrientationShape = PropTypes.shape({ + orientation: PropTypes.number.isRequired, +}); + +export const dynamicOrientationShape = PropTypes.shape({ + field: fieldShape, +}); + export const staticSizeShape = PropTypes.shape({ size: PropTypes.number.isRequired, }); @@ -29,6 +37,8 @@ export const dynamicSizeShape = PropTypes.shape({ export const styleOptionShapes = [ staticColorShape, dynamicColorShape, + staticOrientationShape, + dynamicOrientationShape, staticSizeShape, dynamicSizeShape ]; diff --git a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js index d9ad19347e2be..3ac3dcf3ef76e 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/components/vector/vector_style_editor.js @@ -11,6 +11,7 @@ import chrome from 'ui/chrome'; import { VectorStyleColorEditor } from './color/vector_style_color_editor'; import { VectorStyleSizeEditor } from './size/vector_style_size_editor'; import { VectorStyleSymbolEditor } from './vector_style_symbol_editor'; +import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../../vector_style_defaults'; import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types'; import { SYMBOLIZE_AS_CIRCLE } from '../../vector_constants'; @@ -141,6 +142,7 @@ export class VectorStyleEditor extends Component { _renderPointProperties() { let lineColor; let lineWidth; + let iconOrientation; if (this.props.styleProperties.symbol.options.symbolizeAs === SYMBOLIZE_AS_CIRCLE) { lineColor = ( @@ -154,16 +156,24 @@ export class VectorStyleEditor extends Component { ); + } else { + iconOrientation = ( + + + + + ); } return ( - {this._renderFillColor()} @@ -172,7 +182,17 @@ export class VectorStyleEditor extends Component { {lineWidth} + + + {iconOrientation} + {this._renderSymbolSize()} + ); } diff --git a/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js b/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js index 1ee3582689a8f..26742b13d7011 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/vector_style.js @@ -314,21 +314,26 @@ export class VectorStyle extends AbstractStyle { return (); } - _getScaledFields() { + _getStyleFields() { return this.getDynamicPropertiesArray() .map(({ styleName, options }) => { const name = options.field.name; // "feature-state" data expressions are not supported with layout properties. - // To work around this limitation, some scaled values must fall back to geojson property values. + // To work around this limitation, some styling values must fall back to geojson property values. let supportsFeatureState = true; + let isScaled = true; if (styleName === 'iconSize' && this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) { supportsFeatureState = false; + } else if (styleName === 'iconOrientation') { + supportsFeatureState = false; + isScaled = false; } return { supportsFeatureState, + isScaled, name, range: this._getFieldRange(name), computedName: VectorStyle.getComputedFieldName(name), @@ -355,8 +360,8 @@ export class VectorStyle extends AbstractStyle { return; } - const scaledFields = this._getScaledFields(); - if (scaledFields.length === 0) { + const styleFields = this._getStyleFields(); + if (styleFields.length === 0) { return; } @@ -370,21 +375,30 @@ export class VectorStyle extends AbstractStyle { for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; - for (let j = 0; j < scaledFields.length; j++) { - const { supportsFeatureState, name, range, computedName } = scaledFields[j]; - const unscaledValue = parseFloat(feature.properties[name]); - let scaledValue; - if (isNaN(unscaledValue) || !range) {//cannot scale - scaledValue = -1;//put outside range - } else if (range.delta === 0) {//values are identical - scaledValue = 1;//snap to end of color range + for (let j = 0; j < styleFields.length; j++) { + const { supportsFeatureState, isScaled, name, range, computedName } = styleFields[j]; + const value = parseFloat(feature.properties[name]); + let styleValue; + if (isScaled) { + if (isNaN(value) || !range) {//cannot scale + styleValue = -1;//put outside range + } else if (range.delta === 0) {//values are identical + styleValue = 1;//snap to end of color range + } else { + styleValue = (feature.properties[name] - range.min) / range.delta; + } } else { - scaledValue = (feature.properties[name] - range.min) / range.delta; + if (isNaN(value)) { + styleValue = 0; + } else { + styleValue = value; + } } + if (supportsFeatureState) { - tmpFeatureState[computedName] = scaledValue; + tmpFeatureState[computedName] = styleValue; } else { - feature.properties[computedName] = scaledValue; + feature.properties[computedName] = styleValue; } } tmpFeatureIdentifier.source = sourceId; @@ -392,10 +406,10 @@ export class VectorStyle extends AbstractStyle { mbMap.setFeatureState(tmpFeatureIdentifier, tmpFeatureState); } - const hasScaledGeoJsonProperties = scaledFields.some(({ supportsFeatureState }) => { + const hasGeoJsonProperties = styleFields.some(({ supportsFeatureState }) => { return !supportsFeatureState; }); - return hasScaledGeoJsonProperties; + return hasGeoJsonProperties; } _getMBDataDrivenColor({ fieldName, color }) { @@ -415,7 +429,7 @@ export class VectorStyle extends AbstractStyle { return [ 'interpolate', ['linear'], - ['feature-state', targetName], + ['coalesce', ['feature-state', targetName], 0], 0, minSize, 1, maxSize ]; @@ -554,11 +568,22 @@ export class VectorStyle extends AbstractStyle { mbMap.setLayoutProperty(symbolLayerId, 'icon-size', [ 'interpolate', ['linear'], - ['get', targetName], + ['coalesce', ['get', targetName], 0], 0, iconSize.options.minSize / halfIconPixels, 1, iconSize.options.maxSize / halfIconPixels ]); } + + const iconOrientation = this._descriptor.properties.iconOrientation; + if (iconOrientation.type === VectorStyle.STYLE_TYPE.STATIC) { + mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', iconOrientation.options.orientation); + } else if (_.has(iconOrientation, 'options.field.name')) { + const targetName = VectorStyle.getComputedFieldName(iconOrientation.options.field.name); + // Using property state instead of feature-state because layout properties do not support feature-state + mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', [ + 'coalesce', ['get', targetName], 0 + ]); + } } arePointsSymbolizedAsCircles() { diff --git a/x-pack/plugins/maps/public/shared/layers/styles/vector_style.test.js b/x-pack/plugins/maps/public/shared/layers/styles/vector_style.test.js index 73dc8c541ea72..7c993564018aa 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/vector_style.test.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/vector_style.test.js @@ -49,6 +49,12 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => { options: {}, type: 'STATIC', }, + iconOrientation: { + options: { + orientation: 0, + }, + type: 'STATIC', + }, iconSize: { options: { color: 'a color', diff --git a/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js b/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js index c0475ae2c0bbb..d2262836feea4 100644 --- a/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js +++ b/x-pack/plugins/maps/public/shared/layers/styles/vector_style_defaults.js @@ -56,7 +56,13 @@ export function getDefaultStaticProperties(mapColors = []) { options: { size: DEFAULT_ICON_SIZE } - } + }, + iconOrientation: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + orientation: 0 + } + }, }; } @@ -66,27 +72,37 @@ export function getDefaultDynamicProperties() { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, + field: undefined, } }, lineColor: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, + field: undefined, } }, lineWidth: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, - maxSize: DEFAULT_MAX_SIZE + maxSize: DEFAULT_MAX_SIZE, + field: undefined, } }, iconSize: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, - maxSize: DEFAULT_MAX_SIZE + maxSize: DEFAULT_MAX_SIZE, + field: undefined, } - } + }, + iconOrientation: { + type: VectorStyle.STYLE_TYPE.STATIC, + options: { + field: undefined, + } + }, }; } diff --git a/x-pack/plugins/maps/public/shared/layers/util/import_file.js b/x-pack/plugins/maps/public/shared/layers/util/import_file.js new file mode 100644 index 0000000000000..661e92be3f8bd --- /dev/null +++ b/x-pack/plugins/maps/public/shared/layers/util/import_file.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export async function importFile(file, FileReader = window.FileReader) { + return new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onload = ({ target: { result } }) => { + try { + resolve(JSON.parse(result)); + } catch (e) { + reject(e); + } + }; + fr.readAsText(file); + }); +} diff --git a/x-pack/plugins/maps/public/shared/layers/vector_layer.js b/x-pack/plugins/maps/public/shared/layers/vector_layer.js index b7187dbd63187..1e665b295705e 100644 --- a/x-pack/plugins/maps/public/shared/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/shared/layers/vector_layer.js @@ -441,12 +441,12 @@ export class VectorLayer extends AbstractLayer { mbGeoJSONSource.setData(featureCollection); } - const hasScaledGeoJsonProperties = this._style.setFeatureState(featureCollection, mbMap, this.getId()); + const hasGeoJsonProperties = this._style.setFeatureState(featureCollection, mbMap, this.getId()); // "feature-state" data expressions are not supported with layout properties. // To work around this limitation, // scaled layout properties (like icon-size) must fall back to geojson property values :( - if (hasScaledGeoJsonProperties) { + if (hasGeoJsonProperties) { mbGeoJSONSource.setData(featureCollection); } } diff --git a/x-pack/plugins/maps/public/store/map.js b/x-pack/plugins/maps/public/store/map.js index b5d8b243394d5..633d1dc704189 100644 --- a/x-pack/plugins/maps/public/store/map.js +++ b/x-pack/plugins/maps/public/store/map.js @@ -110,7 +110,7 @@ const INITIAL_STATE = { selectedLayerId: null, __transientLayerId: null, layerList: [], - waitingForMapReadyLayerList: [] + waitingForMapReadyLayerList: [], }; diff --git a/x-pack/plugins/maps/public/store/ui.js b/x-pack/plugins/maps/public/store/ui.js index 6af80c5844b7f..fe8169aa009e0 100644 --- a/x-pack/plugins/maps/public/store/ui.js +++ b/x-pack/plugins/maps/public/store/ui.js @@ -13,6 +13,7 @@ export const SET_FILTERABLE = 'IS_FILTERABLE'; export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; +export const UPDATE_INDEXING_STAGE = 'UPDATE_INDEXING_STAGE'; export const FLYOUT_STATE = { NONE: 'NONE', @@ -20,6 +21,13 @@ export const FLYOUT_STATE = { ADD_LAYER_WIZARD: 'ADD_LAYER_WIZARD' }; +export const INDEXING_STAGE = { + READY: 'READY', + TRIGGERED: 'TRIGGERED', + SUCCESS: 'SUCCESS', + ERROR: 'ERROR', +}; + export const DEFAULT_IS_LAYER_TOC_OPEN = true; const INITIAL_STATE = { @@ -28,9 +36,11 @@ const INITIAL_STATE = { isReadOnly: false, isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN, isFilterable: false, + isSetViewOpen: false, // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], + importIndexingStage: null }; // Reducer @@ -67,6 +77,8 @@ export function ui(state = INITIAL_STATE, action) { return layerId !== action.layerId; }) }; + case UPDATE_INDEXING_STAGE: + return { ...state, importIndexingStage: action.stage }; default: return state; } @@ -142,6 +154,13 @@ export function hideTOCDetails(layerId) { }; } +export function updateIndexingStage(stage) { + return { + type: UPDATE_INDEXING_STAGE, + stage, + }; +} + // Selectors export const getFlyoutDisplay = ({ ui }) => ui && ui.flyoutDisplay || INITIAL_STATE.flyoutDisplay; @@ -151,3 +170,4 @@ export const getOpenTOCDetails = ({ ui }) => ui.openTOCDetails; export const getIsFullScreen = ({ ui }) => ui.isFullScreen; export const getIsReadOnly = ({ ui }) => ui.isReadOnly; export const getIsFilterable = ({ ui }) => ui.isFilterable; +export const getIndexingStage = ({ ui }) => ui.importIndexingStage; diff --git a/x-pack/plugins/ml/index.js b/x-pack/plugins/ml/index.js deleted file mode 100644 index d9f2e30996a91..0000000000000 --- a/x-pack/plugins/ml/index.js +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -import { resolve } from 'path'; -import Boom from 'boom'; -import { checkLicense } from './server/lib/check_license'; -import { addLinksToSampleDatasets } from './server/lib/sample_data_sets'; -import { FEATURE_ANNOTATIONS_ENABLED } from './common/constants/feature_flags'; -import { LICENSE_TYPE } from './common/constants/license'; - -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { annotationRoutes } from './server/routes/annotations'; -import { jobRoutes } from './server/routes/anomaly_detectors'; -import { dataFeedRoutes } from './server/routes/datafeeds'; -import { indicesRoutes } from './server/routes/indices'; -import { jobValidationRoutes } from './server/routes/job_validation'; -import mappings from './mappings'; -import { makeMlUsageCollector } from './server/lib/ml_telemetry'; -import { notificationRoutes } from './server/routes/notification_settings'; -import { systemRoutes } from './server/routes/system'; -import { dataFrameRoutes } from './server/routes/data_frame'; -import { dataRecognizer } from './server/routes/modules'; -import { dataVisualizerRoutes } from './server/routes/data_visualizer'; -import { calendars } from './server/routes/calendars'; -import { fieldsService } from './server/routes/fields_service'; -import { filtersRoutes } from './server/routes/filters'; -import { resultsServiceRoutes } from './server/routes/results_service'; -import { jobServiceRoutes } from './server/routes/job_service'; -import { jobAuditMessagesRoutes } from './server/routes/job_audit_messages'; -import { fileDataVisualizerRoutes } from './server/routes/file_data_visualizer'; -import { i18n } from '@kbn/i18n'; -import { initMlServerLog } from './server/client/log'; - - -export const ml = (kibana) => { - return new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main'], - id: 'ml', - configPrefix: 'xpack.ml', - publicDir: resolve(__dirname, 'public'), - - uiExports: { - app: { - title: i18n.translate('xpack.ml.mlNavTitle', { - defaultMessage: 'Machine Learning' - }), - description: i18n.translate('xpack.ml.mlNavDescription', { - defaultMessage: 'Machine Learning for the Elastic Stack' - }), - icon: 'plugins/ml/ml.svg', - euiIconType: 'machineLearningApp', - main: 'plugins/ml/app', - }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'], - savedObjectSchemas: { - 'ml-telemetry': { - isNamespaceAgnostic: true - } - }, - mappings, - home: ['plugins/ml/register_feature'], - injectDefaultVars(server) { - const config = server.config(); - return { - mlEnabled: config.get('xpack.ml.enabled'), - }; - }, - }, - - init: async function (server) { - const thisPlugin = this; - const xpackMainPlugin = server.plugins.xpack_main; - mirrorPluginStatus(xpackMainPlugin, thisPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - const mlFeature = xpackMainPlugin.info.feature(thisPlugin.id); - mlFeature.registerLicenseCheckResultsGenerator(checkLicense); - - // Add links to the Kibana sample data sets if ml is enabled - // and there is a full license (trial or platinum). - if (mlFeature.isEnabled() === true) { - const licenseCheckResults = mlFeature.getLicenseCheckResults(); - if (licenseCheckResults.licenseType === LICENSE_TYPE.FULL) { - addLinksToSampleDatasets(server); - } - } - }); - - xpackMainPlugin.registerFeature({ - id: 'ml', - name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { - defaultMessage: 'Machine Learning', - }), - icon: 'machineLearningApp', - navLinkId: 'ml', - app: ['ml', 'kibana'], - catalogue: ['ml'], - privileges: {}, - reserved: { - privilege: { - savedObject: { - all: [], - read: [] - }, - ui: [], - }, - description: i18n.translate('xpack.ml.feature.reserved.description', { - defaultMessage: 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.' - }) - } - }); - - // Add server routes and initialize the plugin here - const commonRouteConfig = { - pre: [ - function forbidApiAccess() { - const licenseCheckResults = xpackMainPlugin.info.feature(thisPlugin.id).getLicenseCheckResults(); - if (licenseCheckResults.isAvailable) { - return null; - } else { - throw Boom.forbidden(licenseCheckResults.message); - } - } - ] - }; - - server.injectUiAppVars('ml', () => { - const config = server.config(); - return { - kbnIndex: config.get('kibana.index'), - mlAnnotationsEnabled: FEATURE_ANNOTATIONS_ENABLED, - }; - }); - - annotationRoutes(server, commonRouteConfig); - jobRoutes(server, commonRouteConfig); - dataFeedRoutes(server, commonRouteConfig); - dataFrameRoutes(server, commonRouteConfig); - indicesRoutes(server, commonRouteConfig); - jobValidationRoutes(server, commonRouteConfig); - notificationRoutes(server, commonRouteConfig); - systemRoutes(server, commonRouteConfig); - dataRecognizer(server, commonRouteConfig); - dataVisualizerRoutes(server, commonRouteConfig); - calendars(server, commonRouteConfig); - fieldsService(server, commonRouteConfig); - filtersRoutes(server, commonRouteConfig); - resultsServiceRoutes(server, commonRouteConfig); - jobServiceRoutes(server, commonRouteConfig); - jobAuditMessagesRoutes(server, commonRouteConfig); - fileDataVisualizerRoutes(server, commonRouteConfig); - - initMlServerLog(server); - makeMlUsageCollector(server); - } - - }); -}; - diff --git a/x-pack/plugins/ml/index.ts b/x-pack/plugins/ml/index.ts new file mode 100644 index 0000000000000..230d1ab52df34 --- /dev/null +++ b/x-pack/plugins/ml/index.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolve } from 'path'; +import { i18n } from '@kbn/i18n'; +import KbnServer, { Server } from 'src/legacy/server/kbn_server'; +import { plugin } from './server/new_platform'; +import { + MlInitializerContext, + MlCoreSetup, + MlHttpServiceSetup, +} from './server/new_platform/plugin'; +// @ts-ignore: could not find declaration file for module +import mappings from './mappings'; + +interface MlServer extends Server { + addAppLinksToSampleDataset: () => {}; +} + +export const ml = (kibana: any) => { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + id: 'ml', + configPrefix: 'xpack.ml', + publicDir: resolve(__dirname, 'public'), + + uiExports: { + app: { + title: i18n.translate('xpack.ml.mlNavTitle', { + defaultMessage: 'Machine Learning', + }), + description: i18n.translate('xpack.ml.mlNavDescription', { + defaultMessage: 'Machine Learning for the Elastic Stack', + }), + icon: 'plugins/ml/ml.svg', + euiIconType: 'machineLearningApp', + main: 'plugins/ml/app', + }, + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'], + savedObjectSchemas: { + 'ml-telemetry': { + isNamespaceAgnostic: true, + }, + }, + mappings, + home: ['plugins/ml/register_feature'], + injectDefaultVars(server: any) { + const config = server.config(); + return { + mlEnabled: config.get('xpack.ml.enabled'), + }; + }, + }, + + async init(server: MlServer) { + const kbnServer = (server as unknown) as KbnServer; + + const initializerContext = ({ + legacyConfig: server.config(), + logger: { + get(...contextParts: string[]) { + return kbnServer.newPlatform.coreContext.logger.get('plugins', 'ml', ...contextParts); + }, + }, + } as unknown) as MlInitializerContext; + + const mlHttpService: MlHttpServiceSetup = { + ...kbnServer.newPlatform.setup.core.http, + route: server.route.bind(server), + }; + + const core: MlCoreSetup = { + addAppLinksToSampleDataset: server.addAppLinksToSampleDataset, + injectUiAppVars: server.injectUiAppVars, + http: mlHttpService, + savedObjects: server.savedObjects, + usage: server.usage, + }; + + const plugins = { + elasticsearch: server.plugins.elasticsearch, + security: server.plugins.security, + xpackMain: server.plugins.xpack_main, + ml: this, + }; + + plugin(initializerContext).setup(core, plugins); + }, + }); +}; diff --git a/x-pack/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts b/x-pack/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts index 17652f96a522d..32606d2db425e 100644 --- a/x-pack/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts +++ b/x-pack/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_service.ts @@ -7,10 +7,10 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { Query } from 'ui/embeddable'; import { IndexPattern } from 'ui/index_patterns'; import { toastNotifications } from 'ui/notify'; import { timefilter } from 'ui/timefilter'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { ml } from '../../services/ml_api_service'; export function setFullTimeRange(indexPattern: IndexPattern, query: Query) { diff --git a/x-pack/plugins/ml/public/components/full_time_range_selector/index.test.tsx b/x-pack/plugins/ml/public/components/full_time_range_selector/index.test.tsx index 91c477cb60234..41e8fa014907e 100644 --- a/x-pack/plugins/ml/public/components/full_time_range_selector/index.test.tsx +++ b/x-pack/plugins/ml/public/components/full_time_range_selector/index.test.tsx @@ -6,11 +6,9 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; - -import { Query } from 'ui/embeddable'; -import { QueryLanguageType } from 'ui/embeddable/types'; import { IndexPattern } from 'ui/index_patterns'; import { FullTimeRangeSelector } from './index'; +import { Query } from 'src/legacy/core_plugins/data/public'; // Create a mock for the setFullTimeRange function in the service. // The mock is hoisted to the top, so need to prefix the mock function @@ -30,7 +28,7 @@ describe('FullTimeRangeSelector', () => { }; const query: Query = { - language: QueryLanguageType.KUERY, + language: 'kuery', query: 'region:us-east-1', }; diff --git a/x-pack/plugins/ml/public/components/full_time_range_selector/index.tsx b/x-pack/plugins/ml/public/components/full_time_range_selector/index.tsx index 0e39a0fbc4521..9066fe0a0e8b9 100644 --- a/x-pack/plugins/ml/public/components/full_time_range_selector/index.tsx +++ b/x-pack/plugins/ml/public/components/full_time_range_selector/index.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Query } from 'ui/embeddable'; import { IndexPattern } from 'ui/index_patterns'; import { EuiButton } from '@elastic/eui'; +import { Query } from 'src/legacy/core_plugins/data/public'; import { setFullTimeRange } from './full_time_range_selector_service'; interface Props { diff --git a/x-pack/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js b/x-pack/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js index b58122128f1bd..377bbac981789 100644 --- a/x-pack/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js +++ b/x-pack/plugins/ml/public/components/kql_filter_bar/suggestions/suggestions.test.js @@ -39,13 +39,13 @@ describe('Suggestions', () => { test('is null when show is false', () => { const noShowProps = { ...defaultProps, show: false }; const wrapper = shallow(); - expect(wrapper.html()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('is null when no suggestions are available', () => { const noSuggestions = { ...defaultProps, suggestions: [] }; const wrapper = shallow(); - expect(wrapper.html()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('creates suggestion list item for each suggestion passed in via props', () => { diff --git a/x-pack/plugins/ml/public/data_frame/common/request.test.ts b/x-pack/plugins/ml/public/data_frame/common/request.test.ts index 2f16f2494c90a..dda00aaa0a7cd 100644 --- a/x-pack/plugins/ml/public/data_frame/common/request.test.ts +++ b/x-pack/plugins/ml/public/data_frame/common/request.test.ts @@ -102,7 +102,7 @@ describe('Data Frame: Common', () => { const jobDetailsState: JobDetailsExposedState = { createIndexPattern: false, jobId: 'the-job-id', - targetIndex: 'the-target-index', + destinationIndex: 'the-destination-index', touched: true, valid: true, }; @@ -110,7 +110,7 @@ describe('Data Frame: Common', () => { const request = getDataFrameRequest('the-index-pattern-title', pivotState, jobDetailsState); expect(request).toEqual({ - dest: { index: 'the-target-index' }, + dest: { index: 'the-destination-index' }, pivot: { aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, diff --git a/x-pack/plugins/ml/public/data_frame/common/request.ts b/x-pack/plugins/ml/public/data_frame/common/request.ts index 14aa1cded8ce9..52fdcd4052559 100644 --- a/x-pack/plugins/ml/public/data_frame/common/request.ts +++ b/x-pack/plugins/ml/public/data_frame/common/request.ts @@ -162,7 +162,7 @@ export function getDataFrameRequest( dictionaryToArray(pivotState.aggList) ), dest: { - index: jobDetailsState.targetIndex, + index: jobDetailsState.destinationIndex, }, }; diff --git a/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_form.tsx b/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_form.tsx index c5e41eb445099..b2fe1aa2976b7 100644 --- a/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_form.tsx @@ -19,7 +19,7 @@ import { EsIndexName, IndexPatternTitle, JobId } from './common'; export interface JobDetailsExposedState { createIndexPattern: boolean; jobId: JobId; - targetIndex: EsIndexName; + destinationIndex: EsIndexName; touched: boolean; valid: boolean; } @@ -28,7 +28,7 @@ export function getDefaultJobDetailsState(): JobDetailsExposedState { return { createIndexPattern: true, jobId: '', - targetIndex: '', + destinationIndex: '', touched: false, valid: false, }; @@ -49,7 +49,7 @@ export const JobDetailsForm: SFC = React.memo(({ overrides = {}, onChange const defaults = { ...getDefaultJobDetailsState(), ...overrides }; const [jobId, setJobId] = useState(defaults.jobId); - const [targetIndex, setTargetIndex] = useState(defaults.targetIndex); + const [destinationIndex, setDestinationIndex] = useState(defaults.destinationIndex); const [jobIds, setJobIds] = useState([]); const [indexNames, setIndexNames] = useState([]); const [indexPatternTitles, setIndexPatternTitles] = useState([]); @@ -99,11 +99,11 @@ export const JobDetailsForm: SFC = React.memo(({ overrides = {}, onChange }, []); const jobIdExists = jobIds.some(id => jobId === id); - const indexNameExists = indexNames.some(name => targetIndex === name); - const indexPatternTitleExists = indexPatternTitles.some(name => targetIndex === name); + const indexNameExists = indexNames.some(name => destinationIndex === name); + const indexPatternTitleExists = indexPatternTitles.some(name => destinationIndex === name); const valid = jobId !== '' && - targetIndex !== '' && + destinationIndex !== '' && !jobIdExists && !indexNameExists && (!indexPatternTitleExists || !createIndexPattern); @@ -111,9 +111,9 @@ export const JobDetailsForm: SFC = React.memo(({ overrides = {}, onChange // expose state to wizard useEffect( () => { - onChange({ createIndexPattern, jobId, targetIndex, touched: true, valid }); + onChange({ createIndexPattern, jobId, destinationIndex, touched: true, valid }); }, - [createIndexPattern, jobId, targetIndex, valid] + [createIndexPattern, jobId, destinationIndex, valid] ); return ( @@ -142,26 +142,26 @@ export const JobDetailsForm: SFC = React.memo(({ overrides = {}, onChange /> setTargetIndex(e.target.value)} + placeholder="destination index" + value={destinationIndex} + onChange={e => setDestinationIndex(e.target.value)} aria-label={i18n.translate( - 'xpack.ml.dataframe.jobDetailsForm.targetIndexInputAriaLabel', + 'xpack.ml.dataframe.jobDetailsForm.destinationIndexInputAriaLabel', { - defaultMessage: 'Choose a unique target index name.', + defaultMessage: 'Choose a unique destination index name.', } )} isInvalid={indexNameExists} diff --git a/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_summary.tsx b/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_summary.tsx index b36c9e0723def..9040908a84c81 100644 --- a/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_summary.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/job_details/job_details_summary.tsx @@ -13,12 +13,12 @@ import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { JobDetailsExposedState } from './job_details_form'; export const JobDetailsSummary: SFC = React.memo( - ({ createIndexPattern, jobId, targetIndex, touched }) => { + ({ createIndexPattern, jobId, destinationIndex, touched }) => { if (touched === false) { return null; } - const targetIndexHelpText = createIndexPattern + const destinationIndexHelpText = createIndexPattern ? i18n.translate('xpack.ml.dataframe.jobDetailsSummary.createIndexPatternMessage', { defaultMessage: 'A Kibana index pattern will be created for this job.', }) @@ -34,12 +34,12 @@ export const JobDetailsSummary: SFC = React.memo( - + ); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx index 9fae0e4cf4459..152c540d9d173 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/action_delete.tsx @@ -103,7 +103,7 @@ export const DeleteAction: SFC = ({ deleteJob, item }) => { >

{i18n.translate('xpack.ml.dataframe.jobsList.deleteModalBody', { - defaultMessage: `Are you sure you want to delete this job? The job's target index and optional Kibana index pattern will not be deleted.`, + defaultMessage: `Are you sure you want to delete this job? The job's destination index and optional Kibana index pattern will not be deleted.`, })}

diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.test.tsx index f2d75c9ef7b08..548009c3a2669 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.test.tsx @@ -14,7 +14,7 @@ describe('Data Frame: Job List Columns', () => { expect(columns[0].isExpander).toBeTruthy(); expect(columns[1].name).toBe('ID'); expect(columns[2].name).toBe('Source index'); - expect(columns[3].name).toBe('Target index'); + expect(columns[3].name).toBe('Destination index'); expect(columns[4].name).toBe('Status'); expect(columns[5].name).toBe('Progress'); expect(columns[6].name).toBe('Actions'); diff --git a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.tsx b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.tsx index b74edbaf059b4..2032087a3baa4 100644 --- a/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.tsx +++ b/x-pack/plugins/ml/public/data_frame/pages/job_management/components/job_list/columns.tsx @@ -76,7 +76,9 @@ export const getColumns = ( }, { field: DataFrameJobListColumn.configDestIndex, - name: i18n.translate('xpack.ml.dataframe.targetIndex', { defaultMessage: 'Target index' }), + name: i18n.translate('xpack.ml.dataframe.destinationIndex', { + defaultMessage: 'Destination index', + }), sortable: true, truncateText: true, }, diff --git a/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/__snapshots__/overrides.test.js.snap b/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/__snapshots__/overrides.test.js.snap index d298cf1112876..f303a93d33890 100644 --- a/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/__snapshots__/overrides.test.js.snap +++ b/x-pack/plugins/ml/public/file_datavisualizer/components/edit_flyout/__snapshots__/overrides.test.js.snap @@ -74,6 +74,20 @@ exports[`Overrides render overrides 1`] = ` describedByIds={Array []} fullWidth={false} hasEmptyLabelSpace={false} + helpText={ + + + See more on accepted formats + + + } label={ + + {i18n.translate('xpack.ml.fileDatavisualizer.editFlyout.overrides.timestampFormatHelpText', { + defaultMessage: 'See more on accepted formats' + })} + + + ); return ( @@ -401,6 +416,7 @@ export class Overrides extends Component { } { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); +const _callWithInternalUser = once((elasticsearchPlugin) => { + const { callWithInternalUser } = elasticsearchPlugin.getCluster('admin'); return callWithInternalUser; }); -export const callWithInternalUserFactory = (server) => { +export const callWithInternalUserFactory = (elasticsearchPlugin) => { return (...args) => { - return _callWithInternalUser(server)(...args); + return _callWithInternalUser(elasticsearchPlugin)(...args); }; }; diff --git a/x-pack/plugins/ml/server/client/call_with_internal_user_factory.test.ts b/x-pack/plugins/ml/server/client/call_with_internal_user_factory.test.ts index d77541e7d3d6c..be016cc13ed0f 100644 --- a/x-pack/plugins/ml/server/client/call_with_internal_user_factory.test.ts +++ b/x-pack/plugins/ml/server/client/call_with_internal_user_factory.test.ts @@ -8,25 +8,21 @@ import { callWithInternalUserFactory } from './call_with_internal_user_factory'; describe('call_with_internal_user_factory', () => { describe('callWithInternalUserFactory', () => { - let server: any; + let elasticsearchPlugin: any; let callWithInternalUser: any; beforeEach(() => { callWithInternalUser = jest.fn(); - server = { - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, + elasticsearchPlugin = { + getCluster: jest.fn(() => ({ callWithInternalUser })), }; }); it('should use internal user "admin"', () => { - const callWithInternalUserInstance = callWithInternalUserFactory(server); + const callWithInternalUserInstance = callWithInternalUserFactory(elasticsearchPlugin); callWithInternalUserInstance(); - expect(server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('admin'); + expect(elasticsearchPlugin.getCluster).toHaveBeenCalledWith('admin'); }); }); }); diff --git a/x-pack/plugins/ml/server/client/call_with_request_factory.js b/x-pack/plugins/ml/server/client/call_with_request_factory.js index c7aa44b14dca9..a88731e674b5e 100644 --- a/x-pack/plugins/ml/server/client/call_with_request_factory.js +++ b/x-pack/plugins/ml/server/client/call_with_request_factory.js @@ -9,15 +9,15 @@ import { once } from 'lodash'; import { elasticsearchJsPlugin } from './elasticsearch_ml'; -const callWithRequest = once((server) => { +const callWithRequest = once((elasticsearchPlugin) => { const config = { plugins: [ elasticsearchJsPlugin ] }; - const cluster = server.plugins.elasticsearch.createCluster('ml', config); + const cluster = elasticsearchPlugin.createCluster('ml', config); return cluster.callWithRequest; }); -export const callWithRequestFactory = (server, request) => { +export const callWithRequestFactory = (elasticsearchPlugin, request) => { return (...args) => { - return callWithRequest(server)(request, ...args); + return callWithRequest(elasticsearchPlugin)(request, ...args); }; }; diff --git a/x-pack/plugins/ml/server/client/log.ts b/x-pack/plugins/ml/server/client/log.ts new file mode 100644 index 0000000000000..8ee5882f6c2c1 --- /dev/null +++ b/x-pack/plugins/ml/server/client/log.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from '../../../../../src/core/server'; + +export interface LogInitialization { + log: Logger; +} + +interface MlLog { + fatal: (message: string) => void; + error: (message: string) => void; + warn: (message: string) => void; + info: (message: string) => void; + debug: (message: string) => void; + trace: (message: string) => void; +} + +export let mlLog: MlLog; + +export function initMlServerLog(logInitialization: LogInitialization) { + mlLog = { + fatal: (message: string) => logInitialization.log.fatal(message), + error: (message: string) => logInitialization.log.error(message), + warn: (message: string) => logInitialization.log.warn(message), + info: (message: string) => logInitialization.log.info(message), + debug: (message: string) => logInitialization.log.debug(message), + trace: (message: string) => logInitialization.log.trace(message), + }; +} diff --git a/x-pack/plugins/ml/server/lib/__tests__/security_utils.js b/x-pack/plugins/ml/server/lib/__tests__/security_utils.js index 073b6c3c50b85..aec912a5d27d8 100644 --- a/x-pack/plugins/ml/server/lib/__tests__/security_utils.js +++ b/x-pack/plugins/ml/server/lib/__tests__/security_utils.js @@ -13,17 +13,13 @@ import { describe('ML - security utils', () => { - function mockServerFactory(isAvailable = true, isEnabled = true) { + function mockXpackMainPluginFactory(isAvailable = true, isEnabled = true) { return { - plugins: { - xpack_main: { - info: { - isAvailable: () => isAvailable, - feature: () => ({ - isEnabled: () => isEnabled - }) - } - } + info: { + isAvailable: () => isAvailable, + feature: () => ({ + isEnabled: () => isEnabled + }) } }; } @@ -31,15 +27,15 @@ describe('ML - security utils', () => { describe('isSecurityDisabled', () => { it('returns not disabled for given mock server object #1', () => { - expect(isSecurityDisabled(mockServerFactory())).to.be(false); + expect(isSecurityDisabled(mockXpackMainPluginFactory())).to.be(false); }); it('returns not disabled for given mock server object #2', () => { - expect(isSecurityDisabled(mockServerFactory(false))).to.be(false); + expect(isSecurityDisabled(mockXpackMainPluginFactory(false))).to.be(false); }); it('returns disabled for given mock server object #3', () => { - expect(isSecurityDisabled(mockServerFactory(true, false))).to.be(true); + expect(isSecurityDisabled(mockXpackMainPluginFactory(true, false))).to.be(true); }); }); diff --git a/x-pack/plugins/ml/server/lib/check_annotations/index.js b/x-pack/plugins/ml/server/lib/check_annotations/index.js index efcd6ef54bde7..6878ef6464aa3 100644 --- a/x-pack/plugins/ml/server/lib/check_annotations/index.js +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.js @@ -40,7 +40,7 @@ export async function isAnnotationsFeatureAvailable(callWithRequest) { if (!annotationsWriteAliasExists) return false; } catch (err) { - mlLog('info', 'Disabling ML annotations feature because the index/alias integrity check failed.'); + mlLog.info('Disabling ML annotations feature because the index/alias integrity check failed.'); return false; } diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts index e012b7a06e91d..6bc98ba68f60b 100644 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/make_ml_usage_collector.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; - import { createMlTelemetry, getSavedObjectsClient, @@ -14,23 +12,19 @@ import { MlTelemetrySavedObject, } from './ml_telemetry'; -// TODO this type should be defined by the platform -interface KibanaHapiServer extends Server { - usage: { - collectorSet: { - makeUsageCollector: any; - register: any; - }; - }; -} +import { UsageInitialization } from '../../new_platform/plugin'; -export function makeMlUsageCollector(server: KibanaHapiServer): void { - const mlUsageCollector = server.usage.collectorSet.makeUsageCollector({ +export function makeMlUsageCollector({ + elasticsearchPlugin, + usage, + savedObjects, +}: UsageInitialization): void { + const mlUsageCollector = usage.collectorSet.makeUsageCollector({ type: 'ml', isReady: () => true, fetch: async (): Promise => { try { - const savedObjectsClient = getSavedObjectsClient(server); + const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); const mlTelemetrySavedObject = (await savedObjectsClient.get( 'ml-telemetry', ML_TELEMETRY_DOC_ID @@ -41,5 +35,5 @@ export function makeMlUsageCollector(server: KibanaHapiServer): void { } }, }); - server.usage.collectorSet.register(mlUsageCollector); + usage.collectorSet.register(mlUsageCollector); } diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts index 9a0c3608a893c..fcf3763626b6f 100644 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.test.ts @@ -26,7 +26,8 @@ describe('ml_telemetry', () => { }); describe('storeMlTelemetry', () => { - let server: any; + let elasticsearchPlugin: any; + let savedObjects: any; let mlTelemetry: MlTelemetry; let savedObjectsClientInstance: any; @@ -34,16 +35,12 @@ describe('ml_telemetry', () => { savedObjectsClientInstance = { create: jest.fn() }; const callWithInternalUser = jest.fn(); const internalRepository = jest.fn(); - server = { - savedObjects: { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }, - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, + elasticsearchPlugin = { + getCluster: jest.fn(() => ({ callWithInternalUser })), + }; + savedObjects = { + SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), + getSavedObjectsRepository: jest.fn(() => internalRepository), }; mlTelemetry = { file_data_visualizer: { @@ -53,24 +50,25 @@ describe('ml_telemetry', () => { }); it('should call savedObjectsClient create with the given MlTelemetry object', () => { - storeMlTelemetry(server, mlTelemetry); + storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); expect(savedObjectsClientInstance.create.mock.calls[0][1]).toBe(mlTelemetry); }); it('should call savedObjectsClient create with the ml-telemetry document type and ID', () => { - storeMlTelemetry(server, mlTelemetry); + storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); expect(savedObjectsClientInstance.create.mock.calls[0][2].id).toBe(ML_TELEMETRY_DOC_ID); }); it('should call savedObjectsClient create with overwrite: true', () => { - storeMlTelemetry(server, mlTelemetry); + storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); expect(savedObjectsClientInstance.create.mock.calls[0][2].overwrite).toBe(true); }); }); describe('getSavedObjectsClient', () => { - let server: any; + let elasticsearchPlugin: any; + let savedObjects: any; let savedObjectsClientInstance: any; let callWithInternalUser: any; let internalRepository: any; @@ -79,29 +77,26 @@ describe('ml_telemetry', () => { savedObjectsClientInstance = { create: jest.fn() }; callWithInternalUser = jest.fn(); internalRepository = jest.fn(); - server = { - savedObjects: { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }, - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, + elasticsearchPlugin = { + getCluster: jest.fn(() => ({ callWithInternalUser })), + }; + savedObjects = { + SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), + getSavedObjectsRepository: jest.fn(() => internalRepository), }; }); it('should return a SavedObjectsClient initialized with the saved objects internal repository', () => { - const result = getSavedObjectsClient(server); + const result = getSavedObjectsClient(elasticsearchPlugin, savedObjects); expect(result).toBe(savedObjectsClientInstance); - expect(server.savedObjects.SavedObjectsClient).toHaveBeenCalledWith(internalRepository); + expect(savedObjects.SavedObjectsClient).toHaveBeenCalledWith(internalRepository); }); }); describe('incrementFileDataVisualizerIndexCreationCount', () => { - let server: any; + let elasticsearchPlugin: any; + let savedObjects: any; let savedObjectsClientInstance: any; let callWithInternalUser: any; let internalRepository: any; @@ -147,36 +142,32 @@ describe('ml_telemetry', () => { ); callWithInternalUser = jest.fn(); internalRepository = jest.fn(); - server = { - savedObjects: { - SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), - getSavedObjectsRepository: jest.fn(() => internalRepository), - }, - plugins: { - elasticsearch: { - getCluster: jest.fn(() => ({ callWithInternalUser })), - }, - }, + savedObjects = { + SavedObjectsClient: jest.fn(() => savedObjectsClientInstance), + getSavedObjectsRepository: jest.fn(() => internalRepository), + }; + elasticsearchPlugin = { + getCluster: jest.fn(() => ({ callWithInternalUser })), }; } it('should not increment if telemetry status cannot be determined', async () => { mockInit(); - await incrementFileDataVisualizerIndexCreationCount(server); + await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0); }); it('should not increment if telemetry status is disabled', async () => { mockInit(false); - await incrementFileDataVisualizerIndexCreationCount(server); + await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); expect(savedObjectsClientInstance.create.mock.calls).toHaveLength(0); }); it('should initialize index_creation_count with 1', async () => { mockInit(true); - await incrementFileDataVisualizerIndexCreationCount(server); + await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({ @@ -186,7 +177,7 @@ describe('ml_telemetry', () => { it('should increment index_creation_count to 2', async () => { mockInit(true, 1); - await incrementFileDataVisualizerIndexCreationCount(server); + await incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe('ml-telemetry'); expect(savedObjectsClientInstance.create.mock.calls[0][1]).toEqual({ diff --git a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts index fef0bb2e7617c..442cef6e3bd94 100644 --- a/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts +++ b/x-pack/plugins/ml/server/lib/ml_telemetry/ml_telemetry.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; +import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; +import { SavedObjectsService } from 'src/legacy/server/kbn_server'; import { callWithInternalUserFactory } from '../../client/call_with_internal_user_factory'; export interface MlTelemetry { @@ -26,24 +27,34 @@ export function createMlTelemetry(count: number = 0): MlTelemetry { }, }; } - -export function storeMlTelemetry(server: Server, mlTelemetry: MlTelemetry): void { - const savedObjectsClient = getSavedObjectsClient(server); +// savedObjects +export function storeMlTelemetry( + elasticsearchPlugin: ElasticsearchPlugin, + savedObjects: SavedObjectsService, + mlTelemetry: MlTelemetry +): void { + const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); savedObjectsClient.create('ml-telemetry', mlTelemetry, { id: ML_TELEMETRY_DOC_ID, overwrite: true, }); } - -export function getSavedObjectsClient(server: Server): any { - const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; - const callWithInternalUser = callWithInternalUserFactory(server); +// needs savedObjects and elasticsearchPlugin +export function getSavedObjectsClient( + elasticsearchPlugin: ElasticsearchPlugin, + savedObjects: SavedObjectsService +): any { + const { SavedObjectsClient, getSavedObjectsRepository } = savedObjects; + const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); const internalRepository = getSavedObjectsRepository(callWithInternalUser); return new SavedObjectsClient(internalRepository); } -export async function incrementFileDataVisualizerIndexCreationCount(server: Server): Promise { - const savedObjectsClient = getSavedObjectsClient(server); +export async function incrementFileDataVisualizerIndexCreationCount( + elasticsearchPlugin: ElasticsearchPlugin, + savedObjects: SavedObjectsService +): Promise { + const savedObjectsClient = getSavedObjectsClient(elasticsearchPlugin, savedObjects); try { const { attributes } = await savedObjectsClient.get('telemetry', 'telemetry'); @@ -69,5 +80,5 @@ export async function incrementFileDataVisualizerIndexCreationCount(server: Serv } const mlTelemetry = createMlTelemetry(indicesCount); - storeMlTelemetry(server, mlTelemetry); + storeMlTelemetry(elasticsearchPlugin, savedObjects, mlTelemetry); } diff --git a/x-pack/plugins/ml/server/lib/security_utils.js b/x-pack/plugins/ml/server/lib/security_utils.js index 7fca5a7578452..772ec1d16a72b 100644 --- a/x-pack/plugins/ml/server/lib/security_utils.js +++ b/x-pack/plugins/ml/server/lib/security_utils.js @@ -9,8 +9,7 @@ * Contains utility functions related to x-pack security. */ -export function isSecurityDisabled(server) { - const xpackMainPlugin = server.plugins.xpack_main; +export function isSecurityDisabled(xpackMainPlugin) { const xpackInfo = (xpackMainPlugin && xpackMainPlugin.info); // we assume that `xpack.isAvailable()` always returns `true` because we're inside x-pack // if for whatever reason it returns `false`, `isSecurityDisabled()` would also return `false` diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js index 7b52ed9000e54..c6a7d4665bcd2 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/__tests__/bucket_span_estimator.js @@ -38,25 +38,22 @@ const callWithRequest = (method) => { // we replace the return value of the factory with the above mocked callWithRequest import * as mockModule from '../../../client/call_with_internal_user_factory'; -// mock server -function mockServerFactory(isEnabled = false, licenseType = 'platinum') { +// mock xpack_main plugin +function mockXpackMainPluginFactory(isEnabled = false, licenseType = 'platinum') { return { - plugins: { - xpack_main: { - info: { - isAvailable: () => true, - feature: () => ({ - isEnabled: () => isEnabled - }), - license: { - getType: () => licenseType - } - } + info: { + isAvailable: () => true, + feature: () => ({ + isEnabled: () => isEnabled + }), + license: { + getType: () => licenseType } } }; } +const mockElasticsearchPlugin = {}; // mock configuration to be passed to the estimator const formConfig = { aggTypes: ['count'], @@ -91,7 +88,7 @@ describe('ML - BucketSpanEstimator', () => { it('call factory and estimator with security disabled', (done) => { expect(function () { - const estimateBucketSpan = estimateBucketSpanFactory(callWithRequest, mockServerFactory()); + const estimateBucketSpan = estimateBucketSpanFactory(callWithRequest, mockElasticsearchPlugin, mockXpackMainPluginFactory()); estimateBucketSpan(formConfig).catch((catchData) => { expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets'); @@ -104,8 +101,7 @@ describe('ML - BucketSpanEstimator', () => { it('call factory and estimator with security enabled and sufficient permissions.', (done) => { expect(function () { - const estimateBucketSpan = estimateBucketSpanFactory(callWithRequest, mockServerFactory(true)); - + const estimateBucketSpan = estimateBucketSpanFactory(callWithRequest, mockElasticsearchPlugin, mockXpackMainPluginFactory(true)); estimateBucketSpan(formConfig).catch((catchData) => { expect(catchData).to.be('Unable to retrieve cluster setting search.max_buckets'); mockCallWithInternalUserFactory.verify(); @@ -117,7 +113,7 @@ describe('ML - BucketSpanEstimator', () => { it('call factory and estimator with security enabled and insufficient permissions.', (done) => { expect(function () { - const estimateBucketSpan = estimateBucketSpanFactory(callWithRequest, mockServerFactory(true)); + const estimateBucketSpan = estimateBucketSpanFactory(callWithRequest, mockElasticsearchPlugin, mockXpackMainPluginFactory(true)); estimateBucketSpan(formConfig).catch((catchData) => { expect(catchData).to.be('Insufficient permissions to call bucket span estimation.'); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js index bc20773de7525..6f4d5abddad62 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/bucket_span_estimator.js @@ -15,8 +15,8 @@ import { polledDataCheckerFactory } from './polled_data_checker'; import { callWithInternalUserFactory } from '../../client/call_with_internal_user_factory'; import { isSecurityDisabled } from '../../lib/security_utils'; -export function estimateBucketSpanFactory(callWithRequest, server) { - const callWithInternalUser = callWithInternalUserFactory(server); +export function estimateBucketSpanFactory(callWithRequest, elasticsearchPlugin, xpackMainPlugin) { + const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); const PolledDataChecker = polledDataCheckerFactory(callWithRequest); const SingleSeriesChecker = singleSeriesCheckerFactory(callWithRequest); @@ -372,7 +372,7 @@ export function estimateBucketSpanFactory(callWithRequest, server) { }); } - if (isSecurityDisabled(server)) { + if (isSecurityDisabled(xpackMainPlugin)) { getBucketSpanEstimation(); } else { // if security is enabled, check that the user has permission to diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js index 15b4e86ceafa8..02128ae7ed5f5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.js @@ -68,7 +68,7 @@ export class DataRecognizer { try { file = await this.readFile(`${this.modulesDir}/${dir}/manifest.json`); } catch (error) { - mlLog('warning', `Data recognizer skipping folder ${dir} as manifest.json cannot be read`); + mlLog.warn(`Data recognizer skipping folder ${dir} as manifest.json cannot be read`); } if (file !== undefined) { @@ -78,7 +78,7 @@ export class DataRecognizer { json: JSON.parse(file) }); } catch (error) { - mlLog('warning', `Data recognizer error parsing ${dir}/manifest.json. ${error}`); + mlLog.warn(`Data recognizer error parsing ${dir}/manifest.json. ${error}`); } } @@ -104,7 +104,7 @@ export class DataRecognizer { try { match = await this.searchForFields(moduleConfig, indexPattern); } catch (error) { - mlLog('warning', `Data recognizer error running query defined for module ${moduleConfig.id}. ${error}`); + mlLog.warn(`Data recognizer error running query defined for module ${moduleConfig.id}. ${error}`); } if (match === true) { @@ -194,7 +194,7 @@ export class DataRecognizer { config: JSON.parse(jobConfig) }); } catch (error) { - mlLog('warning', `Data recognizer error loading config for job ${job.id} for module ${id}. ${error}`); + mlLog.warn(`Data recognizer error loading config for job ${job.id} for module ${id}. ${error}`); } })); @@ -211,7 +211,7 @@ export class DataRecognizer { config }); } catch (error) { - mlLog('warning', `Data recognizer error loading config for datafeed ${datafeed.id} for module ${id}. ${error}`); + mlLog.warn(`Data recognizer error loading config for datafeed ${datafeed.id} for module ${id}. ${error}`); } })); @@ -232,7 +232,7 @@ export class DataRecognizer { config }); } catch (error) { - mlLog('warning', `Data recognizer error loading config for ${key} ${obj.id} for module ${id}. ${error}`); + mlLog.warn(`Data recognizer error loading config for ${key} ${obj.id} for module ${id}. ${error}`); } })); })); diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.js b/x-pack/plugins/ml/server/models/job_validation/job_validation.js index 1d59b8c379b98..dfefbff4640e5 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.js +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.js @@ -21,7 +21,7 @@ import { validateInfluencers } from './validate_influencers'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; import { validateTimeRange, isValidTimeField } from './validate_time_range'; -export async function validateJob(callWithRequest, payload, kbnVersion = 'current', server) { +export async function validateJob(callWithRequest, payload, kbnVersion = 'current', elasticsearchPlugin, xpackMainPlugin) { const messages = getMessages(); try { @@ -101,7 +101,7 @@ export async function validateJob(callWithRequest, payload, kbnVersion = 'curren return VALIDATION_STATUS[messages[m.id].status] === VALIDATION_STATUS.ERROR; }); - validationMessages.push(...await validateBucketSpan(callWithRequest, job, duration, server)); + validationMessages.push(...await validateBucketSpan(callWithRequest, job, duration, elasticsearchPlugin, xpackMainPlugin)); validationMessages.push(...await validateTimeRange(callWithRequest, job, duration)); // only run the influencer and model memory limit checks diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js index 6dd9ea34ce433..2a9d3064ee825 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js +++ b/x-pack/plugins/ml/server/models/job_validation/validate_bucket_span.js @@ -48,7 +48,7 @@ const pickBucketSpan = (bucketSpans) => { return bucketSpans[i]; }; -export async function validateBucketSpan(callWithRequest, job, duration, server) { +export async function validateBucketSpan(callWithRequest, job, duration, elasticsearchPlugin, xpackMainPlugin) { validateJobObject(job); // if there is no duration, do not run the estimate test @@ -114,7 +114,7 @@ export async function validateBucketSpan(callWithRequest, job, duration, server) try { const estimations = estimatorConfigs.map((data) => { return new Promise((resolve) => { - estimateBucketSpanFactory(callWithRequest, server)(data) + estimateBucketSpanFactory(callWithRequest, elasticsearchPlugin, xpackMainPlugin)(data) .then(resolve) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. diff --git a/x-pack/plugins/ml/server/new_platform/index.ts b/x-pack/plugins/ml/server/new_platform/index.ts new file mode 100644 index 0000000000000..b03f2dac613b0 --- /dev/null +++ b/x-pack/plugins/ml/server/new_platform/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, MlInitializerContext } from './plugin'; + +export function plugin(initializerContext: MlInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/x-pack/plugins/ml/server/new_platform/plugin.ts b/x-pack/plugins/ml/server/new_platform/plugin.ts new file mode 100644 index 0000000000000..9cbe145804b7c --- /dev/null +++ b/x-pack/plugins/ml/server/new_platform/plugin.ts @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; +import { ServerRoute } from 'hapi'; +import { KibanaConfig, SavedObjectsService } from 'src/legacy/server/kbn_server'; +import { HttpServiceSetup, Logger, PluginInitializerContext } from 'src/core/server'; +import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; +// @ts-ignore: could not find declaration file for module +import { checkLicense } from '../lib/check_license'; +// @ts-ignore: could not find declaration file for module +import { mirrorPluginStatus } from '../../../../server/lib/mirror_plugin_status'; +// @ts-ignore: could not find declaration file for module +import { FEATURE_ANNOTATIONS_ENABLED } from '../../common/constants/feature_flags'; +// @ts-ignore: could not find declaration file for module +import { LICENSE_TYPE } from '../../common/constants/license'; +// @ts-ignore: could not find declaration file for module +import { annotationRoutes } from '../routes/annotations'; +// @ts-ignore: could not find declaration file for module +import { jobRoutes } from '../routes/anomaly_detectors'; +// @ts-ignore: could not find declaration file for module +import { dataFeedRoutes } from '../routes/datafeeds'; +// @ts-ignore: could not find declaration file for module +import { indicesRoutes } from '../routes/indices'; +// @ts-ignore: could not find declaration file for module +import { jobValidationRoutes } from '../routes/job_validation'; +// @ts-ignore: could not find declaration file for module +import { makeMlUsageCollector } from '../lib/ml_telemetry'; +// @ts-ignore: could not find declaration file for module +import { notificationRoutes } from '../routes/notification_settings'; +// @ts-ignore: could not find declaration file for module +import { systemRoutes } from '../routes/system'; +// @ts-ignore: could not find declaration file for module +import { dataFrameRoutes } from '../routes/data_frame'; +// @ts-ignore: could not find declaration file for module +import { dataRecognizer } from '../routes/modules'; +// @ts-ignore: could not find declaration file for module +import { dataVisualizerRoutes } from '../routes/data_visualizer'; +// @ts-ignore: could not find declaration file for module +import { calendars } from '../routes/calendars'; +// @ts-ignore: could not find declaration file for module +import { fieldsService } from '../routes/fields_service'; +// @ts-ignore: could not find declaration file for module +import { filtersRoutes } from '../routes/filters'; +// @ts-ignore: could not find declaration file for module +import { resultsServiceRoutes } from '../routes/results_service'; +// @ts-ignore: could not find declaration file for module +import { jobServiceRoutes } from '../routes/job_service'; +// @ts-ignore: could not find declaration file for module +import { jobAuditMessagesRoutes } from '../routes/job_audit_messages'; +// @ts-ignore: could not find declaration file for module +import { fileDataVisualizerRoutes } from '../routes/file_data_visualizer'; +// @ts-ignore: could not find declaration file for module +import { initMlServerLog, LogInitialization } from '../client/log'; + +export interface MlHttpServiceSetup extends HttpServiceSetup { + route(route: ServerRoute | ServerRoute[]): void; +} + +export interface MlXpackMainPlugin extends XPackMainPlugin { + status?: any; +} + +export interface MlCoreSetup { + addAppLinksToSampleDataset: () => any; + injectUiAppVars: (id: string, callback: () => {}) => any; + http: MlHttpServiceSetup; + savedObjects: SavedObjectsService; + usage: { + collectorSet: { + makeUsageCollector: any; + register: (collector: any) => void; + }; + }; +} +export interface MlInitializerContext extends PluginInitializerContext { + legacyConfig: KibanaConfig; + log: Logger; +} +export interface PluginsSetup { + elasticsearch: ElasticsearchPlugin; + xpackMain: MlXpackMainPlugin; + security: any; + // TODO: this is temporary for `mirrorPluginStatus` + ml: any; +} +export interface RouteInitialization { + commonRouteConfig: any; + config?: any; + elasticsearchPlugin: ElasticsearchPlugin; + route(route: ServerRoute | ServerRoute[]): void; + xpackMainPlugin?: MlXpackMainPlugin; + savedObjects?: SavedObjectsService; +} +export interface UsageInitialization { + elasticsearchPlugin: ElasticsearchPlugin; + usage: { + collectorSet: { + makeUsageCollector: any; + register: (collector: any) => void; + }; + }; + savedObjects: SavedObjectsService; +} + +export class Plugin { + private readonly pluginId: string = 'ml'; + private config: any; + private log: Logger; + + constructor(initializerContext: MlInitializerContext) { + this.config = initializerContext.legacyConfig; + this.log = initializerContext.logger.get(); + } + + public setup(core: MlCoreSetup, plugins: PluginsSetup) { + const xpackMainPlugin: MlXpackMainPlugin = plugins.xpackMain; + const { addAppLinksToSampleDataset, http, injectUiAppVars } = core; + const pluginId = this.pluginId; + + mirrorPluginStatus(xpackMainPlugin, plugins.ml); + xpackMainPlugin.status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + const mlFeature = xpackMainPlugin.info.feature(pluginId); + mlFeature.registerLicenseCheckResultsGenerator(checkLicense); + + // Add links to the Kibana sample data sets if ml is enabled + // and there is a full license (trial or platinum). + if (mlFeature.isEnabled() === true) { + const licenseCheckResults = mlFeature.getLicenseCheckResults(); + if (licenseCheckResults.licenseType === LICENSE_TYPE.FULL) { + addLinksToSampleDatasets({ addAppLinksToSampleDataset }); + } + } + }); + + xpackMainPlugin.registerFeature({ + id: 'ml', + name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { + defaultMessage: 'Machine Learning', + }), + icon: 'machineLearningApp', + navLinkId: 'ml', + app: ['ml', 'kibana'], + catalogue: ['ml'], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + description: i18n.translate('xpack.ml.feature.reserved.description', { + defaultMessage: + 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.', + }), + }, + }); + + // Add server routes and initialize the plugin here + const commonRouteConfig = { + pre: [ + function forbidApiAccess() { + const licenseCheckResults = xpackMainPlugin.info + .feature(pluginId) + .getLicenseCheckResults(); + if (licenseCheckResults.isAvailable) { + return null; + } else { + throw Boom.forbidden(licenseCheckResults.message); + } + }, + ], + }; + + injectUiAppVars('ml', () => { + return { + kbnIndex: this.config.get('kibana.index'), + mlAnnotationsEnabled: FEATURE_ANNOTATIONS_ENABLED, + }; + }); + + const routeInitializationDeps: RouteInitialization = { + commonRouteConfig, + route: http.route, + elasticsearchPlugin: plugins.elasticsearch, + }; + + const extendedRouteInitializationDeps: RouteInitialization = { + ...routeInitializationDeps, + config: this.config, + xpackMainPlugin: plugins.xpackMain, + savedObjects: core.savedObjects, + }; + + const usageInitializationDeps: UsageInitialization = { + elasticsearchPlugin: plugins.elasticsearch, + usage: core.usage, + savedObjects: core.savedObjects, + }; + + const logInitializationDeps: LogInitialization = { + log: this.log, + }; + + annotationRoutes(routeInitializationDeps); + jobRoutes(routeInitializationDeps); + dataFeedRoutes(routeInitializationDeps); + dataFrameRoutes(routeInitializationDeps); + indicesRoutes(routeInitializationDeps); + jobValidationRoutes(extendedRouteInitializationDeps); + notificationRoutes(routeInitializationDeps); + systemRoutes(extendedRouteInitializationDeps); + dataRecognizer(routeInitializationDeps); + dataVisualizerRoutes(routeInitializationDeps); + calendars(routeInitializationDeps); + fieldsService(routeInitializationDeps); + filtersRoutes(routeInitializationDeps); + resultsServiceRoutes(routeInitializationDeps); + jobServiceRoutes(routeInitializationDeps); + jobAuditMessagesRoutes(routeInitializationDeps); + fileDataVisualizerRoutes(extendedRouteInitializationDeps); + + initMlServerLog(logInitializationDeps); + makeMlUsageCollector(usageInitializationDeps); + } + + public stop() {} +} diff --git a/x-pack/plugins/ml/server/routes/annotations.js b/x-pack/plugins/ml/server/routes/annotations.js index 1978dfbfed88e..2dec66b0ae87c 100644 --- a/x-pack/plugins/ml/server/routes/annotations.js +++ b/x-pack/plugins/ml/server/routes/annotations.js @@ -23,12 +23,12 @@ function getAnnotationsFeatureUnavailableErrorMessage() { }) ); } -export function annotationRoutes(server, commonRouteConfig) { - server.route({ +export function annotationRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { + route({ method: 'POST', path: '/api/ml/annotations', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { getAnnotations } = annotationServiceProvider(callWithRequest); return getAnnotations(request.payload) .catch(resp => wrapError(resp)); @@ -38,11 +38,11 @@ export function annotationRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/annotations/index', async handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(callWithRequest); if (annotationsFeatureAvailable === false) { return getAnnotationsFeatureUnavailableErrorMessage(); @@ -58,11 +58,11 @@ export function annotationRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'DELETE', path: '/api/ml/annotations/delete/{annotationId}', async handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable(callWithRequest); if (annotationsFeatureAvailable === false) { return getAnnotationsFeatureUnavailableErrorMessage(); diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.js b/x-pack/plugins/ml/server/routes/anomaly_detectors.js index 6627471a6d0b1..1f0e811607703 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.js +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.js @@ -9,13 +9,13 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; -export function jobRoutes(server, commonRouteConfig) { +export function jobRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'GET', path: '/api/ml/anomaly_detectors', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.jobs') .catch(resp => wrapError(resp)); }, @@ -24,11 +24,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/anomaly_detectors/{jobId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const jobId = request.params.jobId; return callWithRequest('ml.jobs', { jobId }) .catch(resp => wrapError(resp)); @@ -38,11 +38,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/anomaly_detectors/_stats', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.jobStats') .catch(resp => wrapError(resp)); }, @@ -51,11 +51,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/anomaly_detectors/{jobId}/_stats', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const jobId = request.params.jobId; return callWithRequest('ml.jobStats', { jobId }) .catch(resp => wrapError(resp)); @@ -65,11 +65,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/anomaly_detectors/{jobId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const jobId = request.params.jobId; const body = request.payload; return callWithRequest('ml.addJob', { jobId, body }) @@ -80,11 +80,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/anomaly_detectors/{jobId}/_update', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const jobId = request.params.jobId; const body = request.payload; return callWithRequest('ml.updateJob', { jobId, body }) @@ -95,11 +95,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/anomaly_detectors/{jobId}/_open', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const jobId = request.params.jobId; return callWithRequest('ml.openJob', { jobId }) .catch(resp => wrapError(resp)); @@ -109,11 +109,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/anomaly_detectors/{jobId}/_close', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const options = { jobId: request.params.jobId }; @@ -129,11 +129,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'DELETE', path: '/api/ml/anomaly_detectors/{jobId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const options = { jobId: request.params.jobId }; @@ -149,11 +149,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/anomaly_detectors/_validate/detector', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const body = request.payload; return callWithRequest('ml.validateDetector', { body }) .catch(resp => wrapError(resp)); @@ -163,11 +163,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/anomaly_detectors/{jobId}/_forecast', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const jobId = request.params.jobId; const duration = request.payload.duration; return callWithRequest('ml.forecast', { jobId, duration }) @@ -178,11 +178,11 @@ export function jobRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/anomaly_detectors/{jobId}/results/overall_buckets', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.overallBuckets', { jobId: request.params.jobId, top_n: request.payload.topN, diff --git a/x-pack/plugins/ml/server/routes/calendars.js b/x-pack/plugins/ml/server/routes/calendars.js index f389b528730d6..03befdbb4ca9d 100644 --- a/x-pack/plugins/ml/server/routes/calendars.js +++ b/x-pack/plugins/ml/server/routes/calendars.js @@ -36,13 +36,13 @@ function deleteCalendar(callWithRequest, calendarId) { return cal.deleteCalendar(calendarId); } -export function calendars(server, commonRouteConfig) { +export function calendars({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'GET', path: '/api/ml/calendars', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getAllCalendars(callWithRequest) .catch(resp => wrapError(resp)); }, @@ -51,11 +51,11 @@ export function calendars(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/calendars/{calendarId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const calendarId = request.params.calendarId; return getCalendar(callWithRequest, calendarId) .catch(resp => wrapError(resp)); @@ -65,11 +65,11 @@ export function calendars(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/calendars', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const body = request.payload; return newCalendar(callWithRequest, body) .catch(resp => wrapError(resp)); @@ -79,11 +79,11 @@ export function calendars(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/calendars/{calendarId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const calendarId = request.params.calendarId; const body = request.payload; return updateCalendar(callWithRequest, calendarId, body) @@ -94,11 +94,11 @@ export function calendars(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'DELETE', path: '/api/ml/calendars/{calendarId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const calendarId = request.params.calendarId; return deleteCalendar(callWithRequest, calendarId) .catch(resp => wrapError(resp)); diff --git a/x-pack/plugins/ml/server/routes/data_frame.js b/x-pack/plugins/ml/server/routes/data_frame.js index 336e386bb728f..31cd1808cb3ca 100644 --- a/x-pack/plugins/ml/server/routes/data_frame.js +++ b/x-pack/plugins/ml/server/routes/data_frame.js @@ -7,13 +7,13 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; -export function dataFrameRoutes(server, commonRouteConfig) { +export function dataFrameRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'GET', path: '/api/ml/_data_frame/transforms', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.getDataFrameTransforms') .catch(resp => wrapError(resp)); }, @@ -22,11 +22,11 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/_data_frame/transforms/_stats', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.getDataFrameTransformsStats') .catch(resp => wrapError(resp)); }, @@ -35,11 +35,11 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/_data_frame/transforms/{jobId}/_stats', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { jobId } = request.params; return callWithRequest('ml.getDataFrameTransformsStats', { jobId }) .catch(resp => wrapError(resp)); @@ -49,11 +49,11 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/_data_frame/transforms/{jobId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { jobId } = request.params; return callWithRequest('ml.createDataFrameTransformsJob', { body: request.payload, jobId }) .catch(resp => wrapError(resp)); @@ -63,11 +63,11 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'DELETE', path: '/api/ml/_data_frame/transforms/{jobId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { jobId } = request.params; return callWithRequest('ml.deleteDataFrameTransformsJob', { jobId }) .catch(resp => wrapError(resp)); @@ -77,11 +77,11 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/_data_frame/transforms/_preview', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.getDataFrameTransformsPreview', { body: request.payload }) .catch(resp => wrapError(resp)); }, @@ -90,11 +90,11 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/_data_frame/transforms/{jobId}/_start', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { jobId } = request.params; return callWithRequest('ml.startDataFrameTransformsJob', { jobId }) .catch(resp => wrapError(resp)); @@ -104,11 +104,11 @@ export function dataFrameRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/_data_frame/transforms/{jobId}/_stop', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const options = { jobId: request.params.jobId }; diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.js b/x-pack/plugins/ml/server/routes/data_visualizer.js index a8c3e026f6e77..03710d8ee9f03 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.js +++ b/x-pack/plugins/ml/server/routes/data_visualizer.js @@ -60,13 +60,13 @@ function getStatsForFields( } -export function dataVisualizerRoutes(server, commonRouteConfig) { +export function dataVisualizerRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'POST', path: '/api/ml/data_visualizer/get_field_stats/{indexPatternTitle}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const indexPatternTitle = request.params.indexPatternTitle; const payload = request.payload; return getStatsForFields( @@ -87,11 +87,11 @@ export function dataVisualizerRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/data_visualizer/get_overall_stats/{indexPatternTitle}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const indexPatternTitle = request.params.indexPatternTitle; const payload = request.payload; return getOverallStats( diff --git a/x-pack/plugins/ml/server/routes/datafeeds.js b/x-pack/plugins/ml/server/routes/datafeeds.js index 1923bf65c6763..05b181306e04a 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.js +++ b/x-pack/plugins/ml/server/routes/datafeeds.js @@ -9,13 +9,13 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; -export function dataFeedRoutes(server, commonRouteConfig) { +export function dataFeedRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'GET', path: '/api/ml/datafeeds', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.datafeeds') .catch(resp => wrapError(resp)); }, @@ -24,11 +24,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/datafeeds/{datafeedId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const datafeedId = request.params.datafeedId; return callWithRequest('ml.datafeeds', { datafeedId }) .catch(resp => wrapError(resp)); @@ -38,11 +38,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/datafeeds/_stats', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.datafeedStats') .catch(resp => wrapError(resp)); }, @@ -51,11 +51,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/datafeeds/{datafeedId}/_stats', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const datafeedId = request.params.datafeedId; return callWithRequest('ml.datafeedStats', { datafeedId }) .catch(resp => wrapError(resp)); @@ -65,11 +65,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/datafeeds/{datafeedId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const datafeedId = request.params.datafeedId; const body = request.payload; return callWithRequest('ml.addDatafeed', { datafeedId, body }) @@ -80,11 +80,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/datafeeds/{datafeedId}/_update', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const datafeedId = request.params.datafeedId; const body = request.payload; return callWithRequest('ml.updateDatafeed', { datafeedId, body }) @@ -95,11 +95,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'DELETE', path: '/api/ml/datafeeds/{datafeedId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const options = { datafeedId: request.params.datafeedId }; @@ -115,11 +115,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/datafeeds/{datafeedId}/_start', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const datafeedId = request.params.datafeedId; const start = request.payload.start; const end = request.payload.end; @@ -131,11 +131,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/datafeeds/{datafeedId}/_stop', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const datafeedId = request.params.datafeedId; return callWithRequest('ml.stopDatafeed', { datafeedId }) .catch(resp => wrapError(resp)); @@ -145,11 +145,11 @@ export function dataFeedRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/datafeeds/{datafeedId}/_preview', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const datafeedId = request.params.datafeedId; return callWithRequest('ml.datafeedPreview', { datafeedId }) .catch(resp => wrapError(resp)); diff --git a/x-pack/plugins/ml/server/routes/fields_service.js b/x-pack/plugins/ml/server/routes/fields_service.js index 7b3fce2318c32..a72791472401a 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.js +++ b/x-pack/plugins/ml/server/routes/fields_service.js @@ -41,13 +41,13 @@ function getTimeFieldRange(callWithRequest, payload) { query); } -export function fieldsService(server, commonRouteConfig) { +export function fieldsService({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'POST', path: '/api/ml/fields_service/field_cardinality', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getCardinalityOfFields(callWithRequest, request.payload) .catch(resp => wrapError(resp)); }, @@ -56,11 +56,11 @@ export function fieldsService(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/fields_service/time_field_range', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getTimeFieldRange(callWithRequest, request.payload) .catch(resp => wrapError(resp)); }, diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.js b/x-pack/plugins/ml/server/routes/file_data_visualizer.js index 5e3d88d9d1169..628f1936bc09f 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.js +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.js @@ -21,12 +21,12 @@ function importData(callWithRequest, id, index, settings, mappings, ingestPipeli return importDataFunc(id, index, settings, mappings, ingestPipeline, data); } -export function fileDataVisualizerRoutes(server, commonRouteConfig) { - server.route({ +export function fileDataVisualizerRoutes({ commonRouteConfig, elasticsearchPlugin, route, savedObjects }) { + route({ method: 'POST', path: '/api/ml/file_data_visualizer/analyze_file', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const data = request.payload; return analyzeFiles(callWithRequest, data, request.query) @@ -38,11 +38,11 @@ export function fileDataVisualizerRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/file_data_visualizer/import', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { id } = request.query; const { index, data, settings, mappings, ingestPipeline } = request.payload; @@ -50,7 +50,7 @@ export function fileDataVisualizerRoutes(server, commonRouteConfig) { // follow-up import calls to just add additional data will include the `id` of the created // index, we'll ignore those and don't increment the counter. if (id === undefined) { - incrementFileDataVisualizerIndexCreationCount(server); + incrementFileDataVisualizerIndexCreationCount(elasticsearchPlugin, savedObjects); } return importData(callWithRequest, id, index, settings, mappings, ingestPipeline, data) diff --git a/x-pack/plugins/ml/server/routes/filters.js b/x-pack/plugins/ml/server/routes/filters.js index 517b498353d09..48bf57e35469a 100644 --- a/x-pack/plugins/ml/server/routes/filters.js +++ b/x-pack/plugins/ml/server/routes/filters.js @@ -48,13 +48,13 @@ function deleteFilter(callWithRequest, filterId) { return mgr.deleteFilter(filterId); } -export function filtersRoutes(server, commonRouteConfig) { +export function filtersRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'GET', path: '/api/ml/filters', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getAllFilters(callWithRequest) .catch(resp => wrapError(resp)); }, @@ -63,11 +63,11 @@ export function filtersRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/filters/_stats', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getAllFilterStats(callWithRequest) .catch(resp => wrapError(resp)); }, @@ -76,11 +76,11 @@ export function filtersRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/filters/{filterId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const filterId = request.params.filterId; return getFilter(callWithRequest, filterId) .catch(resp => wrapError(resp)); @@ -90,11 +90,11 @@ export function filtersRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/filters', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const body = request.payload; return newFilter(callWithRequest, body) .catch(resp => wrapError(resp)); @@ -104,11 +104,11 @@ export function filtersRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'PUT', path: '/api/ml/filters/{filterId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const filterId = request.params.filterId; const payload = request.payload; return updateFilter( @@ -124,11 +124,11 @@ export function filtersRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'DELETE', path: '/api/ml/filters/{filterId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const filterId = request.params.filterId; return deleteFilter(callWithRequest, filterId) .catch(resp => wrapError(resp)); diff --git a/x-pack/plugins/ml/server/routes/indices.js b/x-pack/plugins/ml/server/routes/indices.js index 899713d42a15e..0bb69b4056aec 100644 --- a/x-pack/plugins/ml/server/routes/indices.js +++ b/x-pack/plugins/ml/server/routes/indices.js @@ -9,13 +9,13 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; -export function indicesRoutes(server, commonRouteConfig) { +export function indicesRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'POST', path: '/api/ml/indices/field_caps', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const index = request.payload.index; let fields = '*'; if (request.payload.fields !== undefined && Array.isArray(request.payload.fields)) { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.js b/x-pack/plugins/ml/server/routes/job_audit_messages.js index b255475babe18..0c9ca993b210a 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.js +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.js @@ -10,12 +10,12 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; -export function jobAuditMessagesRoutes(server, commonRouteConfig) { - server.route({ +export function jobAuditMessagesRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { + route({ method: 'GET', path: '/api/ml/job_audit_messages/messages/{jobId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { getJobAuditMessages } = jobAuditMessagesProvider(callWithRequest); const { jobId } = request.params; const from = request.query.from; @@ -27,11 +27,11 @@ export function jobAuditMessagesRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/job_audit_messages/messages', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { getJobAuditMessages } = jobAuditMessagesProvider(callWithRequest); const from = request.query.from; return getJobAuditMessages(undefined, from) diff --git a/x-pack/plugins/ml/server/routes/job_service.js b/x-pack/plugins/ml/server/routes/job_service.js index 7851274af43cb..dc43f3b3160c8 100644 --- a/x-pack/plugins/ml/server/routes/job_service.js +++ b/x-pack/plugins/ml/server/routes/job_service.js @@ -10,12 +10,12 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; import { jobServiceProvider } from '../models/job_service'; -export function jobServiceRoutes(server, commonRouteConfig) { - server.route({ +export function jobServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { + route({ method: 'POST', path: '/api/ml/jobs/force_start_datafeeds', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { forceStartDatafeeds } = jobServiceProvider(callWithRequest); const { datafeedIds, @@ -30,11 +30,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/stop_datafeeds', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { stopDatafeeds } = jobServiceProvider(callWithRequest); const { datafeedIds } = request.payload; return stopDatafeeds(datafeedIds) @@ -45,11 +45,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/delete_jobs', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { deleteJobs } = jobServiceProvider(callWithRequest); const { jobIds } = request.payload; return deleteJobs(jobIds) @@ -60,11 +60,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/close_jobs', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { closeJobs } = jobServiceProvider(callWithRequest); const { jobIds } = request.payload; return closeJobs(jobIds) @@ -75,11 +75,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/jobs_summary', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { jobsSummary } = jobServiceProvider(callWithRequest); const { jobIds } = request.payload; return jobsSummary(jobIds) @@ -90,11 +90,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/jobs_with_timerange', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { jobsWithTimerange } = jobServiceProvider(callWithRequest); const { dateFormatTz } = request.payload; return jobsWithTimerange(dateFormatTz) @@ -107,11 +107,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/jobs', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { createFullJobsList } = jobServiceProvider(callWithRequest); const { jobIds } = request.payload; return createFullJobsList(jobIds) @@ -122,11 +122,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/jobs/groups', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { getAllGroups } = jobServiceProvider(callWithRequest); return getAllGroups() .catch(resp => wrapError(resp)); @@ -136,11 +136,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/update_groups', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { updateGroups } = jobServiceProvider(callWithRequest); const { jobs } = request.payload; return updateGroups(jobs) @@ -151,11 +151,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/jobs/deleting_jobs_tasks', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { deletingJobTasks } = jobServiceProvider(callWithRequest); return deletingJobTasks() .catch(resp => wrapError(resp)); @@ -165,11 +165,11 @@ export function jobServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/jobs/jobs_exist', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const { jobsExist } = jobServiceProvider(callWithRequest); const { jobIds } = request.payload; return jobsExist(jobIds) diff --git a/x-pack/plugins/ml/server/routes/job_validation.js b/x-pack/plugins/ml/server/routes/job_validation.js index 73e3a8685bb8f..59e2f62f4f62a 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.js +++ b/x-pack/plugins/ml/server/routes/job_validation.js @@ -14,7 +14,7 @@ import { estimateBucketSpanFactory } from '../models/bucket_span_estimator'; import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit'; import { validateJob, validateCardinality } from '../models/job_validation'; -export function jobValidationRoutes(server, commonRouteConfig) { +export function jobValidationRoutes({ commonRouteConfig, config, elasticsearchPlugin, route, xpackMainPlugin }) { function calculateModelMemoryLimit(callWithRequest, payload) { @@ -38,13 +38,13 @@ export function jobValidationRoutes(server, commonRouteConfig) { latestMs); } - server.route({ + route({ method: 'POST', path: '/api/ml/validate/estimate_bucket_span', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); try { - return estimateBucketSpanFactory(callWithRequest, server)(request.payload) + return estimateBucketSpanFactory(callWithRequest, elasticsearchPlugin, xpackMainPlugin)(request.payload) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. // this doesn't return a HTTP error but an object with an error message @@ -65,11 +65,11 @@ export function jobValidationRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/validate/calculate_model_memory_limit', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return calculateModelMemoryLimit(callWithRequest, request.payload) .catch(resp => wrapError(resp)); }, @@ -78,11 +78,11 @@ export function jobValidationRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/validate/cardinality', handler(request, reply) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return validateCardinality(callWithRequest, request.payload) .then(reply) .catch(resp => wrapError(resp)); @@ -92,14 +92,14 @@ export function jobValidationRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/validate/job', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); // pkg.branch corresponds to the version used in documentation links. - const version = server.config().get('pkg.branch'); - return validateJob(callWithRequest, request.payload, version, server) + const version = config.get('pkg.branch'); + return validateJob(callWithRequest, request.payload, version, elasticsearchPlugin, xpackMainPlugin) .catch(resp => wrapError(resp)); }, config: { diff --git a/x-pack/plugins/ml/server/routes/modules.js b/x-pack/plugins/ml/server/routes/modules.js index 67052f331dd0a..85d6ab581b970 100644 --- a/x-pack/plugins/ml/server/routes/modules.js +++ b/x-pack/plugins/ml/server/routes/modules.js @@ -57,13 +57,13 @@ function dataRecognizerJobsExist(callWithRequest, moduleId) { return dr.dataRecognizerJobsExist(moduleId); } -export function dataRecognizer(server, commonRouteConfig) { +export function dataRecognizer({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'GET', path: '/api/ml/modules/recognize/{indexPatternTitle}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const indexPatternTitle = request.params.indexPatternTitle; return recognize(callWithRequest, indexPatternTitle) .catch(resp => wrapError(resp)); @@ -73,11 +73,11 @@ export function dataRecognizer(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/modules/get_module/{moduleId?}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); let moduleId = request.params.moduleId; if (moduleId === '') { // if the endpoint is called with a trailing / @@ -92,11 +92,11 @@ export function dataRecognizer(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/modules/setup/{moduleId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const moduleId = request.params.moduleId; const { @@ -130,11 +130,11 @@ export function dataRecognizer(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/modules/jobs_exist/{moduleId}', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const moduleId = request.params.moduleId; return dataRecognizerJobsExist(callWithRequest, moduleId) .catch(resp => wrapError(resp)); diff --git a/x-pack/plugins/ml/server/routes/notification_settings.js b/x-pack/plugins/ml/server/routes/notification_settings.js index 5ebf08b2055d9..0916743a0519c 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.js +++ b/x-pack/plugins/ml/server/routes/notification_settings.js @@ -9,13 +9,13 @@ import { callWithRequestFactory } from '../client/call_with_request_factory'; import { wrapError } from '../client/errors'; -export function notificationRoutes(server, commonRouteConfig) { +export function notificationRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'GET', path: '/api/ml/notification_settings', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); const params = { includeDefaults: true, filterPath: '**.xpack.notification' diff --git a/x-pack/plugins/ml/server/routes/results_service.js b/x-pack/plugins/ml/server/routes/results_service.js index 0e919e860a7e1..9f1869ed55007 100644 --- a/x-pack/plugins/ml/server/routes/results_service.js +++ b/x-pack/plugins/ml/server/routes/results_service.js @@ -58,13 +58,13 @@ function getCategoryExamples(callWithRequest, payload) { maxExamples); } -export function resultsServiceRoutes(server, commonRouteConfig) { +export function resultsServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - server.route({ + route({ method: 'POST', path: '/api/ml/results/anomalies_table_data', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getAnomaliesTableData(callWithRequest, request.payload) .catch(resp => wrapError(resp)); }, @@ -73,11 +73,11 @@ export function resultsServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/results/category_definition', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getCategoryDefinition(callWithRequest, request.payload) .catch(resp => wrapError(resp)); }, @@ -86,11 +86,11 @@ export function resultsServiceRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/results/category_examples', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return getCategoryExamples(callWithRequest, request.payload) .catch(resp => wrapError(resp)); }, diff --git a/x-pack/plugins/ml/server/routes/system.js b/x-pack/plugins/ml/server/routes/system.js index 9a5de06d82e50..169cca1c99d10 100644 --- a/x-pack/plugins/ml/server/routes/system.js +++ b/x-pack/plugins/ml/server/routes/system.js @@ -15,8 +15,8 @@ import Boom from 'boom'; import { isSecurityDisabled } from '../lib/security_utils'; -export function systemRoutes(server, commonRouteConfig) { - const callWithInternalUser = callWithInternalUserFactory(server); +export function systemRoutes({ commonRouteConfig, elasticsearchPlugin, route, xpackMainPlugin }) { + const callWithInternalUser = callWithInternalUserFactory(elasticsearchPlugin); function getNodeCount() { const filterPath = 'nodes.*.attributes'; @@ -37,11 +37,11 @@ export function systemRoutes(server, commonRouteConfig) { }); } - server.route({ + route({ method: 'POST', path: '/api/ml/_has_privileges', async handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); try { let upgradeInProgress = false; try { @@ -54,13 +54,13 @@ export function systemRoutes(server, commonRouteConfig) { // most likely they do not have the ml_user role and therefore will be blocked from using // ML at all. However, we need to catch this error so the privilege check doesn't fail. if (error.status === 403) { - mlLog('info', 'Unable to determine whether upgrade is being performed due to insufficient user privileges'); + mlLog.info('Unable to determine whether upgrade is being performed due to insufficient user privileges'); } else { - mlLog('warning', 'Unable to determine whether upgrade is being performed'); + mlLog.warn('Unable to determine whether upgrade is being performed'); } } - if (isSecurityDisabled(server)) { + if (isSecurityDisabled(xpackMainPlugin)) { // if xpack.security.enabled has been explicitly set to false // return that security is disabled and don't call the privilegeCheck endpoint return { @@ -82,15 +82,15 @@ export function systemRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/ml_node_count', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return new Promise((resolve, reject) => { // check for basic license first for consistency with other // security disabled checks - if (isSecurityDisabled(server)) { + if (isSecurityDisabled(xpackMainPlugin)) { getNodeCount() .then(resolve) .catch(reject); @@ -133,11 +133,11 @@ export function systemRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'GET', path: '/api/ml/info', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('ml.info') .catch(resp => wrapError(resp)); }, @@ -146,11 +146,11 @@ export function systemRoutes(server, commonRouteConfig) { } }); - server.route({ + route({ method: 'POST', path: '/api/ml/es_search', handler(request) { - const callWithRequest = callWithRequestFactory(server, request); + const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); return callWithRequest('search', request.payload) .catch(resp => wrapError(resp)); }, diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js index f0a603c95e140..b73f52a5fc1ae 100644 --- a/x-pack/plugins/monitoring/public/services/title.js +++ b/x-pack/plugins/monitoring/public/services/title.js @@ -7,11 +7,10 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; -import { DocTitleProvider } from 'ui/doc_title'; +import { docTitle } from 'ui/doc_title'; const uiModule = uiModules.get('monitoring/title', []); -uiModule.service('title', Private => { - const docTitle = Private(DocTitleProvider); +uiModule.service('title', () => { return function changeTitle(cluster, suffix) { let clusterName = _.get(cluster, 'cluster_name'); clusterName = (clusterName) ? `- ${clusterName}` : ''; diff --git a/x-pack/plugins/observability/README.md b/x-pack/plugins/observability/README.md new file mode 100644 index 0000000000000..981af414e15ca --- /dev/null +++ b/x-pack/plugins/observability/README.md @@ -0,0 +1,19 @@ +# Observability Shared Resources + +This "faux" plugin serves as a place to statically share resources, helpers, and components across observability plugins. There is some discussion still happening about the best way to do this, but this is one suggested method that will work for now and has the benefit of adopting our pre-defined build and compile tooling out of the box. + +Files found here can be imported from any other x-pack plugin, with the caveat that these shared components should all be exposed from either `public/index` or `server/index` so that the platform can attempt to monitor breaking changes in this shared API. + +# for a file found at `x-pack/plugins/infra/public/components/Example.tsx` + +```ts +import { ExampleSharedComponent } from '../../../observability/public'; +``` + +### Plugin registration and config + +There is no plugin registration code or config in this folder because it's a "faux" plugin only being used to share code between other plugins. Plugins using this code do not need to register a dependency on this plugin unless this plugin ever exports functionality that relies on Kibana core itself (rather than being static DI components and utilities only, as it is now). + +### Directory structure + +Code meant to be shared by the UI should live in `public/` and be explicity exported from `public/index` while server helpers etc should live in `server/` and be explicitly exported from `server/index`. Code that needs to be shared across client and server should be exported from both places (not put in `common`, etc). diff --git a/x-pack/plugins/observability/public/components/example_shared_component.tsx b/x-pack/plugins/observability/public/components/example_shared_component.tsx new file mode 100644 index 0000000000000..e7cac9e3d7015 --- /dev/null +++ b/x-pack/plugins/observability/public/components/example_shared_component.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +interface Props { + message?: string; +} + +export function ExampleSharedComponent({ message = 'See how it loads.' }: Props) { + return

This is an example of an observability shared component. {message}

; +} diff --git a/x-pack/plugins/observability/public/index.tsx b/x-pack/plugins/observability/public/index.tsx new file mode 100644 index 0000000000000..8052f4a9c02e8 --- /dev/null +++ b/x-pack/plugins/observability/public/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ExampleSharedComponent } from './components/example_shared_component'; diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js index e656e96915958..bfe3373927d7e 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js +++ b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js @@ -19,7 +19,7 @@ export const RollupPrompt = () => (

Kibana's support for rollup index patterns is in beta. You might encounter issues using these patterns in saved searches, visualizations, and dashboards. - They are not supported in advanced features, such as Timeseries, Timelion, + They are not supported in advanced features, such as TSVB, Timelion, and Machine Learning.

diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/register.js b/x-pack/plugins/rollup/public/index_pattern_creation/register.js index c5ce42a3022b2..b14531d8f6efd 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/register.js +++ b/x-pack/plugins/rollup/public/index_pattern_creation/register.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPatternCreationConfigRegistry } from 'ui/management/index_pattern_creation'; +import { addIndexPatternType } from 'ui/management/index_pattern_creation'; import { RollupIndexPatternCreationConfig } from './rollup_index_pattern_creation_config'; export function initIndexPatternCreation() { - IndexPatternCreationConfigRegistry.register(() => RollupIndexPatternCreationConfig); + addIndexPatternType(RollupIndexPatternCreationConfig); } diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 16e12249acd3d..250c8729b5466 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -32,7 +32,7 @@ import { import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper'; import { deepFreeze } from './server/lib/deep_freeze'; -import { createOptionalPlugin } from './server/lib/optional_plugin'; +import { createOptionalPlugin } from '../../server/lib/optional_plugin'; export const security = (kibana) => new kibana.Plugin({ id: 'security', diff --git a/x-pack/plugins/security/public/views/management/edit_role/index.js b/x-pack/plugins/security/public/views/management/edit_role/index.js index fd3d370fd8508..9a2009dc8a8bd 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/plugins/security/public/views/management/edit_role/index.js @@ -81,9 +81,9 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, { const indexPatterns = Private(IndexPatternsProvider); return indexPatterns.getTitles(); }, - spaces($http, chrome, spacesEnabled) { + spaces(spacesEnabled) { if (spacesEnabled) { - return new SpacesManager($http, chrome).getSpaces(); + return new SpacesManager().getSpaces(); } return []; }, diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts index 282fb99ca330a..b6d91b287dd2e 100644 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts @@ -5,7 +5,7 @@ */ import { SpacesPlugin } from '../../../../spaces/types'; -import { OptionalPlugin } from '../optional_plugin'; +import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; test(`checkPrivileges.atSpace when spaces is enabled`, async () => { diff --git a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts index 6b348156a5171..5778ccbc76aff 100644 --- a/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/lib/authorization/check_privileges_dynamically.ts @@ -13,7 +13,7 @@ import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from '. */ import { SpacesPlugin } from '../../../../spaces/types'; -import { OptionalPlugin } from '../optional_plugin'; +import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; export type CheckPrivilegesDynamically = ( privilegeOrPrivileges: string | string[] diff --git a/x-pack/plugins/security/server/lib/authorization/service.ts b/x-pack/plugins/security/server/lib/authorization/service.ts index 493bbe1ccf387..f0274056e2505 100644 --- a/x-pack/plugins/security/server/lib/authorization/service.ts +++ b/x-pack/plugins/security/server/lib/authorization/service.ts @@ -10,7 +10,7 @@ import { getClient } from '../../../../../server/lib/get_client_shield'; import { SpacesPlugin } from '../../../../spaces/types'; import { XPackFeature, XPackMainPlugin } from '../../../../xpack_main/xpack_main'; import { APPLICATION_PREFIX } from '../../../common/constants'; -import { OptionalPlugin } from '../optional_plugin'; +import { OptionalPlugin } from '../../../../../server/lib/optional_plugin'; import { Actions, actionsFactory } from './actions'; import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; import { diff --git a/x-pack/plugins/security/server/routes/api/external/privileges/get.test.ts b/x-pack/plugins/security/server/routes/api/external/privileges/get.test.ts index 405bde6c78db7..16a1b0f7e35a5 100644 --- a/x-pack/plugins/security/server/routes/api/external/privileges/get.test.ts +++ b/x-pack/plugins/security/server/routes/api/external/privileges/get.test.ts @@ -7,6 +7,7 @@ import Boom from 'boom'; import { Server } from 'hapi'; import { RawKibanaPrivileges } from '../../../../../common/model'; import { initGetPrivilegesApi } from './get'; +import { AuthorizationService } from '../../../../lib/authorization/service'; const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => { return { @@ -37,13 +38,13 @@ const createMockServer = () => { const mockServer = new Server({ debug: false, port: 8080 }); mockServer.plugins.security = { - authorization: { + authorization: ({ privileges: { get: jest.fn().mockImplementation(() => { return createRawKibanaPrivileges(); }), }, - }, + } as unknown) as AuthorizationService, } as any; return mockServer; }; diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts index 28b6d3df32b7f..17ab05d0a33b8 100644 --- a/x-pack/plugins/siem/common/utility_types.ts +++ b/x-pack/plugins/siem/common/utility_types.ts @@ -7,3 +7,5 @@ export type Pick3 = { [P1 in K1]: { [P2 in K2]: { [P3 in K3]: ((T[K1])[K2])[P3] } } }; + +export type Omit = Pick>; diff --git a/x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap b/x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap new file mode 100644 index 0000000000000..aef15188e3698 --- /dev/null +++ b/x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AreaChartBaseComponent render with customized configs should 2 render AreaSeries 1`] = `[Function]`; + +exports[`AreaChartBaseComponent render with default configs if no customized configs given should 2 render AreaSeries 1`] = `[Function]`; diff --git a/x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap b/x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap new file mode 100644 index 0000000000000..12b9afb661da1 --- /dev/null +++ b/x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BarChartBaseComponent render with customized configs should 2 render BarSeries 1`] = `[Function]`; + +exports[`BarChartBaseComponent render with default configs if no customized configs given should 2 render BarSeries 1`] = `[Function]`; diff --git a/x-pack/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/plugins/siem/public/components/charts/areachart.test.tsx index 2221b819e2b4c..fb438e0c9852c 100644 --- a/x-pack/plugins/siem/public/components/charts/areachart.test.tsx +++ b/x-pack/plugins/siem/public/components/charts/areachart.test.tsx @@ -4,30 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, ReactWrapper } from 'enzyme'; +import { ShallowWrapper, shallow } from 'enzyme'; import * as React from 'react'; import { AreaChartBaseComponent, AreaChartWithCustomPrompt } from './areachart'; -import { ChartConfigsData } from './common'; +import { ChartConfigsData, ChartHolder } from './common'; +import { ScaleType, AreaSeries, Axis } from '@elastic/charts'; + +jest.mock('@elastic/charts'); describe('AreaChartBaseComponent', () => { - let wrapper: ReactWrapper; + let shallowWrapper: ShallowWrapper; const mockAreaChartData: ChartConfigsData[] = [ { key: 'uniqueSourceIpsHistogram', value: [ - { x: 1556686800000, y: 580213 }, - { x: 1556730000000, y: 1096175 }, - { x: 1556773200000, y: 12382 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1096175 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 }, ], color: '#DB1374', }, { key: 'uniqueDestinationIpsHistogram', value: [ - { x: 1556686800000, y: 565975 }, - { x: 1556730000000, y: 1084366 }, - { x: 1556773200000, y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, ], color: '#490092', }, @@ -35,47 +38,165 @@ describe('AreaChartBaseComponent', () => { describe('render', () => { beforeAll(() => { - wrapper = mount(); + shallowWrapper = shallow( + + ); }); it('should render Chart', () => { - expect(wrapper.find('Chart')).toHaveLength(1); + expect(shallowWrapper.find('Chart')).toHaveLength(1); + }); + }); + + describe('render with customized configs', () => { + const mockTimeFormatter = jest.fn(); + const mockNumberFormatter = jest.fn(); + const configs = { + series: { + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }, + axis: { + xTickFormatter: mockTimeFormatter, + yTickFormatter: mockNumberFormatter, + }, + }; + + beforeAll(() => { + shallowWrapper = shallow( + + ); + }); + + it(`should ${mockAreaChartData.length} render AreaSeries`, () => { + expect(shallow).toMatchSnapshot(); + expect(shallowWrapper.find(AreaSeries)).toHaveLength(mockAreaChartData.length); + }); + + it('should render AreaSeries with given xScaleType', () => { + expect( + shallowWrapper + .find(AreaSeries) + .first() + .prop('xScaleType') + ).toEqual(configs.series.xScaleType); + }); + + it('should render AreaSeries with given yScaleType', () => { + expect( + shallowWrapper + .find(AreaSeries) + .first() + .prop('yScaleType') + ).toEqual(configs.series.yScaleType); + }); + + it('should render xAxis with given tick formatter', () => { + expect( + shallowWrapper + .find(Axis) + .first() + .prop('tickFormat') + ).toEqual(mockTimeFormatter); + }); + + it('should render yAxis with given tick formatter', () => { + expect( + shallowWrapper + .find(Axis) + .last() + .prop('tickFormat') + ).toEqual(mockNumberFormatter); + }); + }); + + describe('render with default configs if no customized configs given', () => { + beforeAll(() => { + shallowWrapper = shallow( + + ); + }); + + it(`should ${mockAreaChartData.length} render AreaSeries`, () => { + expect(shallow).toMatchSnapshot(); + expect(shallowWrapper.find(AreaSeries)).toHaveLength(mockAreaChartData.length); + }); + + it('should render AreaSeries with default xScaleType: Linear', () => { + expect( + shallowWrapper + .find(AreaSeries) + .first() + .prop('xScaleType') + ).toEqual(ScaleType.Linear); + }); + + it('should render AreaSeries with default yScaleType: Linear', () => { + expect( + shallowWrapper + .find(AreaSeries) + .first() + .prop('yScaleType') + ).toEqual(ScaleType.Linear); + }); + + it('should not format xTicks value', () => { + expect( + shallowWrapper + .find(Axis) + .last() + .prop('tickFormat') + ).toBeUndefined(); + }); + + it('should not format yTicks value', () => { + expect( + shallowWrapper + .find(Axis) + .last() + .prop('tickFormat') + ).toBeUndefined(); }); }); describe('no render', () => { beforeAll(() => { - wrapper = mount( + shallowWrapper = shallow( ); }); it('should not render without height and width', () => { - expect(wrapper.find('Chart')).toHaveLength(0); + expect(shallowWrapper.find('Chart')).toHaveLength(0); }); }); }); describe('AreaChartWithCustomPrompt', () => { - let wrapper: ReactWrapper; + let shallowWrapper: ShallowWrapper; describe.each([ [ [ { key: 'uniqueSourceIpsHistogram', value: [ - { x: 1556686800000, y: 580213 }, - { x: 1556730000000, y: 1096175 }, - { x: 1556773200000, y: 12382 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1096175 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 }, ], color: '#DB1374', }, { key: 'uniqueDestinationIpsHistogram', value: [ - { x: 1556686800000, y: 565975 }, - { x: 1556730000000, y: 1084366 }, - { x: 1556773200000, y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, ], color: '#490092', }, @@ -90,9 +211,9 @@ describe('AreaChartWithCustomPrompt', () => { { key: 'uniqueDestinationIpsHistogram', value: [ - { x: 1556686800000, y: 565975 }, - { x: 1556730000000, y: 1084366 }, - { x: 1556773200000, y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, ], color: '#490092', }, @@ -101,12 +222,12 @@ describe('AreaChartWithCustomPrompt', () => { ], ])('renders areachart', (data: ChartConfigsData[] | [] | null | undefined) => { beforeAll(() => { - wrapper = mount(); + shallowWrapper = shallow(); }); it('render AreaChartBaseComponent', () => { - expect(wrapper.find('Chart')).toHaveLength(1); - expect(wrapper.find('ChartHolder')).toHaveLength(0); + expect(shallowWrapper.find(AreaChartBaseComponent)).toHaveLength(1); + expect(shallowWrapper.find(ChartHolder)).toHaveLength(0); }); }); @@ -128,12 +249,20 @@ describe('AreaChartWithCustomPrompt', () => { [ { key: 'uniqueSourceIpsHistogram', - value: [{ x: 1556686800000 }, { x: 1556730000000 }, { x: 1556773200000 }], + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').valueOf() }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf() }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf() }, + ], color: '#DB1374', }, { key: 'uniqueDestinationIpsHistogram', - value: [{ x: 1556686800000 }, { x: 1556730000000 }, { x: 1556773200000 }], + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').valueOf() }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf() }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf() }, + ], color: '#490092', }, ], @@ -142,18 +271,18 @@ describe('AreaChartWithCustomPrompt', () => { { key: 'uniqueSourceIpsHistogram', value: [ - { x: 1556686800000, y: 580213 }, - { x: 1556730000000, y: null }, - { x: 1556773200000, y: 12382 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: null }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 }, ], color: '#DB1374', }, { key: 'uniqueDestinationIpsHistogram', value: [ - { x: 1556686800000, y: 565975 }, - { x: 1556730000000, y: 1084366 }, - { x: 1556773200000, y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, ], color: '#490092', }, @@ -164,18 +293,18 @@ describe('AreaChartWithCustomPrompt', () => { { key: 'uniqueSourceIpsHistogram', value: [ - { x: 1556686800000, y: 580213 }, - { x: 1556730000000, y: {} }, - { x: 1556773200000, y: 12382 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 580213 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: {} }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12382 }, ], color: '#DB1374', }, { key: 'uniqueDestinationIpsHistogram', value: [ - { x: 1556686800000, y: 565975 }, - { x: 1556730000000, y: 1084366 }, - { x: 1556773200000, y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, ], color: '#490092', }, @@ -183,12 +312,12 @@ describe('AreaChartWithCustomPrompt', () => { ], ])('renders prompt', (data: ChartConfigsData[] | [] | null | undefined) => { beforeAll(() => { - wrapper = mount(); + shallowWrapper = shallow(); }); it('render Chart Holder', () => { - expect(wrapper.find('Chart')).toHaveLength(0); - expect(wrapper.find('ChartHolder')).toHaveLength(1); + expect(shallowWrapper.find(AreaChartBaseComponent)).toHaveLength(0); + expect(shallowWrapper.find(ChartHolder)).toHaveLength(1); }); }); }); diff --git a/x-pack/plugins/siem/public/components/charts/areachart.tsx b/x-pack/plugins/siem/public/components/charts/areachart.tsx index 0e2c74509f2c0..4784e3dc45d1c 100644 --- a/x-pack/plugins/siem/public/components/charts/areachart.tsx +++ b/x-pack/plugins/siem/public/components/charts/areachart.tsx @@ -15,21 +15,18 @@ import { ScaleType, Settings, } from '@elastic/charts'; -import '@elastic/charts/dist/style.css'; +import { getOr, get } from 'lodash/fp'; import { ChartConfigsData, ChartHolder, getSeriesStyle, - numberFormatter, WrappedByAutoSizer, getTheme, + ChartSeriesConfigs, + browserTimezone, } from './common'; import { AutoSizer } from '../auto_sizer'; -const dateFormatter = (d: string) => { - return d.toLocaleString().split('T')[0]; -}; - // custom series styles: https://ela.st/areachart-styling const getSeriesLineStyle = (color: string | undefined) => { return color @@ -65,7 +62,13 @@ export const AreaChartBaseComponent = React.memo<{ data: ChartConfigsData[]; width: number | null | undefined; height: number | null | undefined; + configs?: ChartSeriesConfigs | undefined; }>(({ data, ...chartConfigs }) => { + const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs); + const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs); + const xAxisId = getAxisId(`group-${data[0].key}-x`); + const yAxisId = getAxisId(`group-${data[0].key}-y`); + return chartConfigs.width && chartConfigs.height ? (

@@ -79,8 +82,9 @@ export const AreaChartBaseComponent = React.memo<{ key={seriesKey} name={series.key.replace('Histogram', '')} data={series.value} - xScaleType={ScaleType.Ordinal} - yScaleType={ScaleType.Linear} + xScaleType={getOr(ScaleType.Linear, 'configs.series.xScaleType', chartConfigs)} + yScaleType={getOr(ScaleType.Linear, 'configs.series.yScaleType', chartConfigs)} + timeZone={browserTimezone} xAccessor="x" yAccessors={['y']} areaSeriesStyle={getSeriesLineStyle(series.color)} @@ -89,19 +93,23 @@ export const AreaChartBaseComponent = React.memo<{ ) : null; })} - - + {xTickFormatter ? ( + + ) : ( + + )} + + {yTickFormatter ? ( + + ) : ( + + )}
) : null; @@ -111,7 +119,8 @@ export const AreaChartWithCustomPrompt = React.memo<{ data: ChartConfigsData[] | null | undefined; height: number | null | undefined; width: number | null | undefined; -}>(({ data, height, width }) => { + configs?: ChartSeriesConfigs | undefined; +}>(({ data, height, width, configs }) => { return data != null && data.length && data.every( @@ -120,20 +129,26 @@ export const AreaChartWithCustomPrompt = React.memo<{ value.length > 0 && value.every(chart => chart.x != null && chart.y != null) ) ? ( - + ) : ( ); }); -export const AreaChart = React.memo<{ areaChart: ChartConfigsData[] | null | undefined }>( - ({ areaChart }) => ( - - {({ measureRef, content: { height, width } }) => ( - - - - )} - - ) -); +export const AreaChart = React.memo<{ + areaChart: ChartConfigsData[] | null | undefined; + configs?: ChartSeriesConfigs | undefined; +}>(({ areaChart, configs }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + )} + +)); diff --git a/x-pack/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/plugins/siem/public/components/charts/barchart.test.tsx index 854aabed8aadc..7693a6c6d925d 100644 --- a/x-pack/plugins/siem/public/components/charts/barchart.test.tsx +++ b/x-pack/plugins/siem/public/components/charts/barchart.test.tsx @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, ReactWrapper } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import * as React from 'react'; import { BarChartBaseComponent, BarChartWithCustomPrompt } from './barchart'; -import { ChartConfigsData } from './common'; +import { ChartConfigsData, ChartHolder } from './common'; +import { BarSeries, ScaleType, Axis } from '@elastic/charts'; + +jest.mock('@elastic/charts'); describe('BarChartBaseComponent', () => { - let wrapper: ReactWrapper; + let shallowWrapper: ShallowWrapper; const mockBarChartData: ChartConfigsData[] = [ { key: 'uniqueSourceIps', @@ -27,21 +30,134 @@ describe('BarChartBaseComponent', () => { describe('render', () => { beforeAll(() => { - wrapper = mount(); + shallowWrapper = shallow( + + ); }); it('should render two bar series', () => { - expect(wrapper.find('Chart')).toHaveLength(1); + expect(shallowWrapper.find('Chart')).toHaveLength(1); + }); + }); + + describe('render with customized configs', () => { + const mockNumberFormatter = jest.fn(); + const configs = { + series: { + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }, + axis: { + yTickFormatter: mockNumberFormatter, + }, + }; + + beforeAll(() => { + shallowWrapper = shallow( + + ); + }); + + it(`should ${mockBarChartData.length} render BarSeries`, () => { + expect(shallow).toMatchSnapshot(); + expect(shallowWrapper.find(BarSeries)).toHaveLength(mockBarChartData.length); + }); + + it('should render BarSeries with given xScaleType', () => { + expect( + shallowWrapper + .find(BarSeries) + .first() + .prop('xScaleType') + ).toEqual(configs.series.xScaleType); + }); + + it('should render BarSeries with given yScaleType', () => { + expect( + shallowWrapper + .find(BarSeries) + .first() + .prop('yScaleType') + ).toEqual(configs.series.yScaleType); + }); + + it('should render xAxis with given tick formatter', () => { + expect( + shallowWrapper + .find(Axis) + .first() + .prop('tickFormat') + ).toBeUndefined(); + }); + + it('should render yAxis with given tick formatter', () => { + expect( + shallowWrapper + .find(Axis) + .last() + .prop('tickFormat') + ).toEqual(mockNumberFormatter); + }); + }); + + describe('render with default configs if no customized configs given', () => { + beforeAll(() => { + shallowWrapper = shallow( + + ); + }); + + it(`should ${mockBarChartData.length} render BarSeries`, () => { + expect(shallow).toMatchSnapshot(); + expect(shallowWrapper.find(BarSeries)).toHaveLength(mockBarChartData.length); + }); + + it('should render BarSeries with default xScaleType: Linear', () => { + expect( + shallowWrapper + .find(BarSeries) + .first() + .prop('xScaleType') + ).toEqual(ScaleType.Linear); + }); + + it('should render BarSeries with default yScaleType: Linear', () => { + expect( + shallowWrapper + .find(BarSeries) + .first() + .prop('yScaleType') + ).toEqual(ScaleType.Linear); + }); + + it('should not format xTicks value', () => { + expect( + shallowWrapper + .find(Axis) + .last() + .prop('tickFormat') + ).toBeUndefined(); + }); + + it('should not format yTicks value', () => { + expect( + shallowWrapper + .find(Axis) + .last() + .prop('tickFormat') + ).toBeUndefined(); }); }); describe('no render', () => { beforeAll(() => { - wrapper = mount(); + shallowWrapper = shallow( + + ); }); it('should not render without height and width', () => { - expect(wrapper.find('Chart')).toHaveLength(0); + expect(shallowWrapper.find('Chart')).toHaveLength(0); }); }); }); @@ -78,17 +194,17 @@ describe.each([ ], ], ])('BarChartWithCustomPrompt', mockBarChartData => { - let wrapper: ReactWrapper; + let shallowWrapper: ShallowWrapper; describe('renders barchart', () => { beforeAll(() => { - wrapper = mount( + shallowWrapper = shallow( ); }); it('render BarChartBaseComponent', () => { - expect(wrapper.find('Chart')).toHaveLength(1); - expect(wrapper.find('ChartHolder')).toHaveLength(0); + expect(shallowWrapper.find(BarChartBaseComponent)).toHaveLength(1); + expect(shallowWrapper.find(ChartHolder)).toHaveLength(0); }); }); }); @@ -156,13 +272,13 @@ describe.each([ ], ], ])('renders prompt', (data: ChartConfigsData[] | [] | null | undefined) => { - let wrapper: ReactWrapper; + let shallowWrapper: ShallowWrapper; beforeAll(() => { - wrapper = mount(); + shallowWrapper = shallow(); }); it('render Chart Holder', () => { - expect(wrapper.find('Chart')).toHaveLength(0); - expect(wrapper.find('ChartHolder')).toHaveLength(1); + expect(shallowWrapper.find(BarChartBaseComponent)).toHaveLength(0); + expect(shallowWrapper.find(ChartHolder)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/siem/public/components/charts/barchart.tsx b/x-pack/plugins/siem/public/components/charts/barchart.tsx index 5068a1985a139..f63b1699e2f4d 100644 --- a/x-pack/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/plugins/siem/public/components/charts/barchart.tsx @@ -8,14 +8,16 @@ import React from 'react'; import { Chart, BarSeries, Axis, Position, getSpecId, ScaleType, Settings } from '@elastic/charts'; import { getAxisId } from '@elastic/charts'; +import { getOr, get } from 'lodash/fp'; import { ChartConfigsData, WrappedByAutoSizer, ChartHolder, - numberFormatter, SeriesType, getSeriesStyle, getTheme, + ChartSeriesConfigs, + browserTimezone, } from './common'; import { AutoSizer } from '../auto_sizer'; @@ -24,7 +26,13 @@ export const BarChartBaseComponent = React.memo<{ data: ChartConfigsData[]; width: number | null | undefined; height: number | null | undefined; + configs?: ChartSeriesConfigs | undefined; }>(({ data, ...chartConfigs }) => { + const xTickFormatter = get('configs.axis.xTickFormatter', chartConfigs); + const yTickFormatter = get('configs.axis.yTickFormatter', chartConfigs); + const xAxisId = getAxisId(`stat-items-barchart-${data[0].key}-x`); + const yAxisId = getAxisId(`stat-items-barchart-${data[0].key}-y`); + return chartConfigs.width && chartConfigs.height ? ( @@ -37,10 +45,11 @@ export const BarChartBaseComponent = React.memo<{ id={barSeriesSpecId} key={barSeriesKey} name={series.key} - xScaleType={ScaleType.Ordinal} - yScaleType={ScaleType.Linear} + xScaleType={getOr(ScaleType.Linear, 'configs.series.xScaleType', chartConfigs)} + yScaleType={getOr(ScaleType.Linear, 'configs.series.yScaleType', chartConfigs)} xAccessor="x" yAccessors={['y']} + timeZone={browserTimezone} splitSeriesAccessors={['g']} data={series.value!} stackAccessors={['y']} @@ -49,17 +58,23 @@ export const BarChartBaseComponent = React.memo<{ ); })} - - + {xTickFormatter ? ( + + ) : ( + + )} + + {yTickFormatter ? ( + + ) : ( + + )} ) : null; }); @@ -68,27 +83,29 @@ export const BarChartWithCustomPrompt = React.memo<{ data: ChartConfigsData[] | null | undefined; height: number | null | undefined; width: number | null | undefined; -}>(({ data, height, width }) => { + configs?: ChartSeriesConfigs | undefined; +}>(({ data, height, width, configs }) => { return data && data.length && data.some( ({ value }) => value != null && value.length > 0 && value.every(chart => chart.y != null && chart.y > 0) ) ? ( - + ) : ( ); }); -export const BarChart = React.memo<{ barChart: ChartConfigsData[] | null | undefined }>( - ({ barChart }) => ( - - {({ measureRef, content: { height, width } }) => ( - - - - )} - - ) -); +export const BarChart = React.memo<{ + barChart: ChartConfigsData[] | null | undefined; + configs?: ChartSeriesConfigs | undefined; +}>(({ barChart, configs }) => ( + + {({ measureRef, content: { height, width } }) => ( + + + + )} + +)); diff --git a/x-pack/plugins/siem/public/components/charts/common.tsx b/x-pack/plugins/siem/public/components/charts/common.tsx index cbae797cd977e..c8646900e5250 100644 --- a/x-pack/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/plugins/siem/public/components/charts/common.tsx @@ -12,8 +12,14 @@ import { getSpecId, mergeWithDefaultTheme, PartialTheme, + LIGHT_THEME, + DARK_THEME, + ScaleType, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; +import { TickFormatter } from '@elastic/charts/dist/lib/series/specs'; +import chrome from 'ui/chrome'; +import moment from 'moment-timezone'; const chartHeight = 74; const FlexGroup = styled(EuiFlexGroup)` @@ -39,10 +45,23 @@ export interface ChartData { g?: number | string; } +export interface ChartSeriesConfigs { + series?: { + xScaleType?: ScaleType | undefined; + yScaleType?: ScaleType | undefined; + }; + axis?: { + xTickFormatter?: TickFormatter | undefined; + yTickFormatter?: TickFormatter | undefined; + }; +} + export interface ChartConfigsData { key: string; value: ChartData[] | [] | null; color?: string | undefined; + areachartConfigs?: ChartSeriesConfigs | undefined; + barchartConfigs?: ChartSeriesConfigs | undefined; } export const WrappedByAutoSizer = styled.div` @@ -54,10 +73,6 @@ export const WrappedByAutoSizer = styled.div` } `; -export const numberFormatter = (value: string | number) => { - return value.toLocaleString && value.toLocaleString(); -}; - export enum SeriesType { BAR = 'bar', AREA = 'area', @@ -102,5 +117,10 @@ export const getTheme = () => { barsPadding: 0.5, }, }; - return mergeWithDefaultTheme(theme); + const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); + const defaultTheme = isDarkMode ? DARK_THEME : LIGHT_THEME; + return mergeWithDefaultTheme(theme, defaultTheme); }; + +const kibanaTimezone = chrome.getUiSettingsClient().get('dateFormat:tz'); +export const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; diff --git a/x-pack/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/plugins/siem/public/components/draggables/index.test.tsx index 9f51c0eb79eb3..fb49329ba1501 100644 --- a/x-pack/plugins/siem/public/components/draggables/index.test.tsx +++ b/x-pack/plugins/siem/public/components/draggables/index.test.tsx @@ -113,7 +113,7 @@ describe('draggables', () => { ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { @@ -122,7 +122,7 @@ describe('draggables', () => { ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it renders a tooltip with the field name if a tooltip is not explicitly provided', () => { @@ -229,7 +229,7 @@ describe('draggables', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { @@ -244,7 +244,7 @@ describe('draggables', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns Empty string text if value is an empty string', () => { diff --git a/x-pack/plugins/siem/public/components/draggables/index.tsx b/x-pack/plugins/siem/public/components/draggables/index.tsx index 56659960461af..be4e7dd72acab 100644 --- a/x-pack/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/plugins/siem/public/components/draggables/index.tsx @@ -9,6 +9,7 @@ import * as React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; +import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { getEmptyStringTag } from '../empty_value'; @@ -120,8 +121,6 @@ const Badge = styled(EuiBadge)` vertical-align: top; `; -type Omit = Pick>; - export type BadgeDraggableType = Omit & { contextId: string; eventId: string; diff --git a/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx index 10bd3b8613c0f..bd2bdc314a938 100644 --- a/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx +++ b/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx @@ -12,7 +12,7 @@ import * as React from 'react'; import { AppTestingFrameworkAdapter } from '../../lib/adapters/framework/testing_framework_adapter'; import { mockFrameworks, TestProviders } from '../../mock'; -import { PreferenceFormattedDate, FormattedDate, getMaybeDate } from '.'; +import { PreferenceFormattedDate, FormattedDate } from '.'; import { getEmptyValue } from '../empty_value'; import { KibanaConfigContext } from '../../lib/adapters/framework/kibana_framework_adapter'; @@ -155,56 +155,4 @@ describe('formatted_date', () => { }); }); }); - - describe('getMaybeDate', () => { - test('returns empty string as invalid date', () => { - expect(getMaybeDate('').isValid()).toBe(false); - }); - - test('returns string with empty spaces as invalid date', () => { - expect(getMaybeDate(' ').isValid()).toBe(false); - }); - - test('returns string date time as valid date', () => { - expect(getMaybeDate('2019-05-28T23:05:28.405Z').isValid()).toBe(true); - }); - - test('returns string date time as the date we expect', () => { - expect(getMaybeDate('2019-05-28T23:05:28.405Z').toISOString()).toBe( - '2019-05-28T23:05:28.405Z' - ); - }); - - test('returns plain string number as epoch as valid date', () => { - expect(getMaybeDate('1559084770612').isValid()).toBe(true); - }); - - test('returns plain string number as the date we expect', () => { - expect( - getMaybeDate('1559084770612') - .toDate() - .toISOString() - ).toBe('2019-05-28T23:06:10.612Z'); - }); - - test('returns plain number as epoch as valid date', () => { - expect(getMaybeDate(1559084770612).isValid()).toBe(true); - }); - - test('returns plain number as epoch as the date we expect', () => { - expect( - getMaybeDate(1559084770612) - .toDate() - .toISOString() - ).toBe('2019-05-28T23:06:10.612Z'); - }); - - test('returns a short date time string as an epoch (sadly) so this is ambiguous', () => { - expect( - getMaybeDate('20190101') - .toDate() - .toISOString() - ).toBe('1970-01-01T05:36:30.101Z'); - }); - }); }); diff --git a/x-pack/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.tsx index d6206358c0d04..ac4b289b5eae2 100644 --- a/x-pack/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/plugins/siem/public/components/formatted_date/index.tsx @@ -9,13 +9,13 @@ import * as React from 'react'; import { useContext } from 'react'; import { pure } from 'recompose'; -import { isString } from 'lodash/fp'; import { AppKibanaFrameworkAdapter, KibanaConfigContext, } from '../../lib/adapters/framework/kibana_framework_adapter'; import { getOrEmptyTagFromValue } from '../empty_value'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; +import { getMaybeDate } from './maybe_date'; export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => { const config: Partial = useContext(KibanaConfigContext); @@ -30,19 +30,6 @@ export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => { ); }); -export const getMaybeDate = (value: string | number): moment.Moment => { - if (isString(value) && value.trim() !== '') { - const maybeDate = moment(new Date(value)); - if (maybeDate.isValid() || isNaN(+value)) { - return maybeDate; - } else { - return moment(new Date(+value)); - } - } else { - return moment(new Date(value)); - } -}; - /** * Renders the specified date value in a format determined by the user's preferences, * with a tooltip that renders: diff --git a/x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts new file mode 100644 index 0000000000000..7919f10cc53b3 --- /dev/null +++ b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getMaybeDate } from './maybe_date'; + +describe('#getMaybeDate', () => { + test('returns empty string as invalid date', () => { + expect(getMaybeDate('').isValid()).toBe(false); + }); + + test('returns string with empty spaces as invalid date', () => { + expect(getMaybeDate(' ').isValid()).toBe(false); + }); + + test('returns string date time as valid date', () => { + expect(getMaybeDate('2019-05-28T23:05:28.405Z').isValid()).toBe(true); + }); + + test('returns string date time as the date we expect', () => { + expect(getMaybeDate('2019-05-28T23:05:28.405Z').toISOString()).toBe('2019-05-28T23:05:28.405Z'); + }); + + test('returns plain string number as epoch as valid date', () => { + expect(getMaybeDate('1559084770612').isValid()).toBe(true); + }); + + test('returns plain string number as the date we expect', () => { + expect( + getMaybeDate('1559084770612') + .toDate() + .toISOString() + ).toBe('2019-05-28T23:06:10.612Z'); + }); + + test('returns plain number as epoch as valid date', () => { + expect(getMaybeDate(1559084770612).isValid()).toBe(true); + }); + + test('returns plain number as epoch as the date we expect', () => { + expect( + getMaybeDate(1559084770612) + .toDate() + .toISOString() + ).toBe('2019-05-28T23:06:10.612Z'); + }); + + test('returns a short date time string as an epoch (sadly) so this is ambiguous', () => { + expect( + getMaybeDate('20190101') + .toDate() + .toISOString() + ).toBe('1970-01-01T05:36:30.101Z'); + }); +}); diff --git a/x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts new file mode 100644 index 0000000000000..39df281c6091d --- /dev/null +++ b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isString } from 'lodash/fp'; +import moment from 'moment'; + +export const getMaybeDate = (value: string | number): moment.Moment => { + if (isString(value) && value.trim() !== '') { + const maybeDate = moment(new Date(value)); + if (maybeDate.isValid() || isNaN(+value)) { + return maybeDate; + } else { + return moment(new Date(+value)); + } + } else { + return moment(new Date(value)); + } +}; diff --git a/x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap index 01be0e9b193cc..62e3144c759c0 100644 --- a/x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`KpiNetwork Component rendering it renders loading icons 1`] = ` - - - -
+ + - -
- -
- HOSTS -
-
- -
- + HOSTS + + + +
- -
- -
- - -
+ + - - -

+ + - -- - -

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

+ -- + +

+ + +
+
+
+
+ +
+ + +
+ + +
+ +
+ + +
+ + + `; exports[`Stat Items Component disable charts it renders the default widget 2`] = ` - - - -
+ + - -
- -
- HOSTS -
-
- -
- + HOSTS + + + +
- -
- -
- 0 - - -
+ 0 + + - - -

+ + - -- - -

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

+ -- + +

+ + +
+
+
+
+ + +
+
+ + + +
+ +
+ + + + + + `; exports[`Stat Items Component rendering kpis with charts it renders the default widget 1`] = ` - - - -
+ + - -
- -
- UNIQUE_PRIVATE_IPS -
-
- -
- + UNIQUE_PRIVATE_IPS + + + +
- -
- -
- - -
- - - - - -
-
-
- - -
+ + + +
+
+
+ + - - -

+ + - 1,714 - - Source -

-
-
-
- - -
- -
- -
- - + 1,714 + + Source +

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

+ + - 2,359 - - Dest. -

-
-
-
- - -
-
- - - - - - -
-
- -
- - -
+ 2,359 + + Dest. +

+ + +
+
+
+
+
+ + + + + + +
+
+ +
+ + - + +
+ + + + +
- - + -
- - - -
-
-
- -
-
-
- - -
- - - -
- - - -
-
-
-
-
-
-
- -
- - - - - -
+ ], + }, + ] + } + configs={ + Object { + "axis": Object { + "xTickFormatter": [Function], + "yTickFormatter": [Function], + }, + "series": Object { + "xScaleType": "time", + "yScaleType": "linear", + }, + } + } + /> + + + + + + + + + + + + + + `; diff --git a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx index 8c175e34217f3..d7e4f7040a620 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx @@ -14,39 +14,53 @@ import { addValueToFields, addValueToAreaChart, addValueToBarChart, + useKpiMatrixStatus, + StatItems, } from '.'; import { BarChart } from '../charts/barchart'; import { AreaChart } from '../charts/areachart'; import { EuiHorizontalRule } from '@elastic/eui'; -import { fieldTitleChartMapping, KpiNetworkBaseComponent } from '../page/network/kpi_network'; +import { fieldTitleChartMapping } from '../page/network/kpi_network'; import { mockData, - mockNoChartMappings, - mockDisableChartsInitialData, mockEnableChartsData, - mockEnableChartsInitialData, + mockNoChartMappings, } from '../page/network/kpi_network/mock'; +import { mockGlobalState, apolloClientObservable } from '../../mock'; +import { State, createStore } from '../../store'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { KpiNetworkData, KpiHostsData } from '../../graphql/types'; +jest.mock('../charts/barchart'); +jest.mock('../charts/areachart'); describe('Stat Items Component', () => { + const state: State = mockGlobalState; + + const store = createStore(state, apolloClientObservable); + describe.each([ [ mount( - + + + ), ], [ mount( - + + + ), ], ])('disable charts', wrapper => { @@ -99,18 +113,18 @@ describe('Stat Items Component', () => { { key: 'uniqueSourceIpsHistogram', value: [ - { x: 1556686800000, y: 580213 }, - { x: 1556730000000, y: 1096175 }, - { x: 1556773200000, y: 12382 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, ], color: '#DB1374', }, { key: 'uniqueDestinationIpsHistogram', value: [ - { x: 1556686800000, y: 565975 }, - { x: 1556730000000, y: 1084366 }, - { x: 1556773200000, y: 12280 }, + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, ], color: '#490092', }, @@ -128,7 +142,11 @@ describe('Stat Items Component', () => { }; let wrapper: ReactWrapper; beforeAll(() => { - wrapper = mount(); + wrapper = mount( + + + + ); }); test('it renders the default widget', () => { expect(toJson(wrapper)).toMatchSnapshot(); @@ -186,41 +204,52 @@ describe('addValueToBarChart', () => { describe('useKpiMatrixStatus', () => { const mockNetworkMappings = fieldTitleChartMapping; const mockKpiNetworkData = mockData.KpiNetwork; + const MockChildComponent = (mappedStatItemProps: StatItemsProps) => ; + const MockHookWrapperComponent = ({ + fieldsMapping, + data, + }: { + fieldsMapping: Readonly; + data: KpiNetworkData | KpiHostsData; + }) => { + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(fieldsMapping, data); + + return ( +
+ {statItemsProps.map(mappedStatItemProps => { + return ; + })} +
+ ); + }; test('it updates status correctly', () => { const wrapper = mount( - + <> + + ); - expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockEnableChartsInitialData); - wrapper.setProps({ data: mockKpiNetworkData }); - wrapper.update(); - expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockEnableChartsData); + expect(wrapper.find('MockChildComponent').get(0).props).toEqual(mockEnableChartsData); }); test('it should not append areaChart if enableAreaChart is off', () => { - const mockNetworkMappingsNoAreaChart = mockNoChartMappings; - const wrapper = mount( - + <> + + ); - expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockDisableChartsInitialData); - wrapper.setProps({ data: mockKpiNetworkData }); - wrapper.update(); - expect(wrapper.find(StatItemsComponent).get(0).props.areaChart).toBeUndefined(); + expect(wrapper.find('MockChildComponent').get(0).props.areaChart).toBeUndefined(); }); test('it should not append barChart if enableBarChart is off', () => { - const mockNetworkMappingsNoAreaChart = mockNoChartMappings; - const wrapper = mount( - + <> + + ); - expect(wrapper.find(StatItemsComponent).get(0).props).toEqual(mockDisableChartsInitialData); - wrapper.setProps({ data: mockKpiNetworkData }); - wrapper.update(); - expect(wrapper.find(StatItemsComponent).get(0).props.barChart).toBeUndefined(); + expect(wrapper.find('MockChildComponent').get(0).props.barChart).toBeUndefined(); }); }); diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx index 7641348ad359c..0bf4b7fa28048 100644 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ b/x-pack/plugins/siem/public/components/stat_items/index.tsx @@ -17,11 +17,13 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { get, getOr } from 'lodash/fp'; +import { ScaleType, niceTimeFormatter } from '@elastic/charts'; import { BarChart } from '../charts/barchart'; import { AreaChart } from '../charts/areachart'; import { getEmptyTagValue } from '../empty_value'; -import { ChartConfigsData, ChartData } from '../charts/common'; +import { ChartConfigsData, ChartData, ChartSeriesConfigs } from '../charts/common'; import { KpiHostsData, KpiNetworkData } from '../../graphql/types'; +import { GlobalTime } from '../../containers/global_time'; const FlexItem = styled(EuiFlexItem)` min-width: 0; @@ -49,6 +51,8 @@ export interface StatItems { enableAreaChart?: boolean; enableBarChart?: boolean; grow?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true | false | null; + areachartConfigs?: ChartSeriesConfigs; + barchartConfigs?: ChartSeriesConfigs; } export interface StatItemsProps extends StatItems { @@ -56,6 +60,27 @@ export interface StatItemsProps extends StatItems { barChart?: ChartConfigsData[]; } +export const numberFormatter = (value: string | number): string => value.toLocaleString(); +export const areachartConfigs = (from: number, to: number) => ({ + series: { + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }, + axis: { + xTickFormatter: niceTimeFormatter([from, to]), + yTickFormatter: numberFormatter, + }, +}); +export const barchartConfigs = { + series: { + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + }, + axis: { + xTickFormatter: numberFormatter, + }, +}; + export const addValueToFields = ( fields: StatItem[], data: KpiHostsData | KpiNetworkData @@ -129,7 +154,7 @@ export const useKpiMatrixStatus = ( export const StatItemsComponent = React.memo( ({ fields, description, grow, barChart, areaChart, enableAreaChart, enableBarChart }) => { - const isBarChartDataAbailable = + const isBarChartDataAvailable = barChart && barChart.length && barChart.every(item => item.value != null && item.value.length > 0); @@ -148,7 +173,7 @@ export const StatItemsComponent = React.memo( {fields.map(field => ( - {(isAreaChartDataAvailable || isBarChartDataAbailable) && field.icon && ( + {(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && ( ( {enableBarChart && ( - + )} {enableAreaChart && ( - + + {({ from, to }) => ( + + )} + )} diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx index 299e7c0d2d5d8..9583fef3a5737 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx @@ -38,7 +38,7 @@ describe('Args', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if args is null', () => { @@ -52,7 +52,7 @@ describe('Args', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns empty string if args happens to be an empty string', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx index 4ed1ce0f0cfdb..7aaa04d5aae9d 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx @@ -57,7 +57,7 @@ describe('GenericDetails', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx index fc037e5d8fc1c..aa9c1d5971bcc 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx @@ -62,7 +62,7 @@ describe('GenericFileDetails', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx index b8c5075d5baaf..3cbe5b50f2cb2 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx @@ -39,7 +39,7 @@ describe('ProcessDraggable', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if everything is undefined', () => { @@ -54,7 +54,7 @@ describe('ProcessDraggable', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns process name if that is all that is passed in', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 2dc61d6582ae8..03375be710c81 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -40,7 +40,7 @@ describe('SuricataDetails', () => { ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index 227196971732d..512d02b364b2a 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -35,7 +35,7 @@ describe('SuricataSignature', () => { describe('Tokens', () => { test('should render empty if tokens are empty', () => { const wrapper = mountWithIntl(); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('should render a single if it is present', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx index 9b9291580f5e5..c12cdeb694cd3 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx @@ -39,7 +39,7 @@ describe('AuthSsh', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if sshSignature and sshMethod are both undefined', () => { @@ -53,7 +53,7 @@ describe('AuthSsh', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if sshSignature is null and sshMethod is undefined', () => { @@ -67,7 +67,7 @@ describe('AuthSsh', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if sshSignature is undefined and sshMethod is null', () => { @@ -81,7 +81,7 @@ describe('AuthSsh', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns sshSignature if sshMethod is null', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx index 1b7e50d0b0d39..b88d5ad0bab92 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx @@ -39,7 +39,7 @@ describe('Package', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if all of the package information is undefined ', () => { @@ -54,7 +54,7 @@ describe('Package', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns just the package name', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx index 7644f8269bda5..8b0df5c64d45a 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -39,7 +39,7 @@ describe('UserHostWorkingDir', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if userName, hostName, and workingDirectory are all undefined', () => { @@ -54,7 +54,7 @@ describe('UserHostWorkingDir', () => { /> ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns userName if that is the only attribute defined', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx index f3712c018402d..2cae95518b627 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -94,7 +94,7 @@ describe('ZeekDetails', () => { ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index f1f3a95b5ff06..5711d800fcfed 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -71,7 +71,7 @@ describe('ZeekSignature', () => { describe('#TotalVirusLinkSha', () => { test('should return null if value is null', () => { const wrapper = mountWithIntl(); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('should render value', () => { @@ -88,7 +88,7 @@ describe('ZeekSignature', () => { describe('#Link', () => { test('should return null if value is null', () => { const wrapper = mountWithIntl(); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('should render value', () => { @@ -111,7 +111,7 @@ describe('ZeekSignature', () => { ); - expect(wrapper.text()).toBeNull(); + expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it renders the default ZeekSignature', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx index 3fb527927c543..b9b31b65f8e6b 100644 --- a/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -154,7 +154,7 @@ const LargeNotesButton = pure<{ noteIds: string[]; text?: string; toggleShowNote toggleShowNotes()} - size="l" + size="m" > diff --git a/x-pack/plugins/siem/public/components/toasters/index.test.tsx b/x-pack/plugins/siem/public/components/toasters/index.test.tsx index 0a729d6b8e79f..1e89956f07ad8 100644 --- a/x-pack/plugins/siem/public/components/toasters/index.test.tsx +++ b/x-pack/plugins/siem/public/components/toasters/index.test.tsx @@ -59,7 +59,7 @@ describe('Toaster', () => { if (toasts.length === 0) { dispatch({ type: 'addToaster', toast: mockToast }); } - }); + }, []); return ( <>