diff --git a/.eslintrc.js b/.eslintrc.js index 14567f43430060..ce4aea3c023092 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -510,6 +510,10 @@ module.exports = { // typescript and javascript for front and back end files: ['x-pack/legacy/plugins/siem/**/*.{js,ts,tsx}'], plugins: ['eslint-plugin-node', 'react'], + env: { + mocha: true, + jest: true, + }, rules: { 'accessor-pairs': 'error', 'array-callback-return': 'error', @@ -564,8 +568,7 @@ module.exports = { 'no-shadow-restricted-names': 'error', 'no-sparse-arrays': 'error', 'no-this-before-super': 'error', - // This will be turned on after bug fixes are mostly complete - // 'no-undef': 'warn', + 'no-undef': 'error', 'no-unreachable': 'error', 'no-unsafe-finally': 'error', 'no-useless-call': 'error', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 516f5e71577e6d..bea2f4e74297e8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,6 +13,8 @@ # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui +/x-pack/test/functional/apps/apm/ @elastic/apm-ui +/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui # Beats /x-pack/legacy/plugins/beats_management/ @elastic/beats diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 152a0e2c488712..5fd3ef5e8ff4bf 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -141,6 +141,39 @@ function addBar(foos, foo) { } ``` +### Avoid `any` whenever possible + +Since TypeScript 3.0 and the introduction of the +[`unknown` type](https://mariusschulz.com/blog/the-unknown-type-in-typescript) there are rarely any +reasons to use `any` as a type. Nearly all places of former `any` usage can be replace by either a +generic or `unknown` (in cases the type is really not known). + +You should always prefer using those mechanisms over using `any`, since they are stricter typed and +less likely to introduce bugs in the future due to insufficient types. + +If you’re not having `any` in your plugin or are starting a new plugin, you should enable the +[`@typescript-eslint/no-explicit-any`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md) +linting rule for your plugin via the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. + +### Avoid non-null assertions + +You should try avoiding non-null assertions (`!.`) wherever possible. By using them you tell +TypeScript, that something is not null even though by it’s type it could be. Usage of non-null +assertions is most often a side-effect of you actually checked that the variable is not `null` +but TypeScript doesn’t correctly carry on that information till the usage of the variable. + +In most cases it’s possible to replace the non-null assertion by structuring your code/checks slightly different +or using [user defined type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) +to properly tell TypeScript what type a variable has. + +Using non-null assertion increases the risk for future bugs. In case the condition under which we assumed that the +variable can’t be null has changed (potentially even due to changes in compeltely different files), the non-null +assertion would now wrongly disable proper type checking for us. + +If you’re not using non-null assertions in your plugin or are starting a new plugin, consider enabling the +[`@typescript-eslint/no-non-null-assertion`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-non-null-assertion.md) +linting rule for you plugin in the [`.eslintrc.js`](https://github.com/elastic/kibana/blob/master/.eslintrc.js) config. + ### Return/throw early from functions To avoid deep nesting of if-statements, always return a function's value as early diff --git a/config/kibana.yml b/config/kibana.yml index 7d49fb37e03208..9525a6423d90a0 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -18,10 +18,6 @@ # default to `true` starting in Kibana 7.0. #server.rewriteBasePath: false -# Specifies the default route when opening Kibana. You can use this setting to modify -# the landing page when opening Kibana. -#server.defaultRoute: /app/kibana - # The maximum payload size in bytes for incoming server requests. #server.maxPayloadBytes: 1048576 diff --git a/docs/api/saved-objects/find.asciidoc b/docs/api/saved-objects/find.asciidoc index fd80951b1c9f25..f20ded78e07434 100644 --- a/docs/api/saved-objects/find.asciidoc +++ b/docs/api/saved-objects/find.asciidoc @@ -41,6 +41,11 @@ experimental[] Retrieve a paginated set of {kib} saved objects by various condit `has_reference`:: (Optional, object) Filters to objects that have a relationship with the type and ID combination. +`filter`:: + (Optional, string) The filter is a KQL string with the caveat that if you filter with an attribute from your type saved object. + It should look like that savedObjectType.attributes.title: "myTitle". However, If you used a direct attribute of a saved object like `updatedAt`, + you will have to define your filter like that savedObjectType.updatedAt > 2018-12-22. + NOTE: As objects change in {kib}, the results on each page of the response also change. Use the find API for traditional paginated results, but avoid using it to export large amounts of data. diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md index 0b5bd8eeb36eca..f264ba500ed6ef 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md @@ -9,7 +9,7 @@ Register a context provider for application mounting. Will only be available to Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters @@ -17,7 +17,7 @@ registerMountContext(contextName: T, provider: | Parameter | Type | Description | | --- | --- | --- | | contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<AppMountContext, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | +| provider | IContextProvider<App['mount'], T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md index fc86aaf658b681..62821fcbb92bad 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md @@ -9,7 +9,7 @@ Register a context provider for application mounting. Will only be available to Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters @@ -17,7 +17,7 @@ registerMountContext(contextName: T, provider: | Parameter | Type | Description | | --- | --- | --- | | contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<AppMountContext, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | +| provider | IContextProvider<App['mount'], T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md b/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md index 2644596354e383..5334eee8425779 100644 --- a/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md +++ b/docs/development/core/public/kibana-plugin-public.contextsetup.createcontextcontainer.md @@ -9,9 +9,9 @@ Creates a new [IContextContainer](./kibana-plugin-public.icontextcontainer.md) f Signature: ```typescript -createContextContainer(): IContextContainer; +createContextContainer>(): IContextContainer; ``` Returns: -`IContextContainer` +`IContextContainer` diff --git a/docs/development/core/public/kibana-plugin-public.handlercontexttype.md b/docs/development/core/public/kibana-plugin-public.handlercontexttype.md new file mode 100644 index 00000000000000..b083449d2b703f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.handlercontexttype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) + +## HandlerContextType type + +Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. + +Signature: + +```typescript +export declare type HandlerContextType> = T extends HandlerFunction ? U : never; +``` diff --git a/docs/development/core/public/kibana-plugin-public.handlerfunction.md b/docs/development/core/public/kibana-plugin-public.handlerfunction.md new file mode 100644 index 00000000000000..98c342c17691d9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.handlerfunction.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HandlerFunction](./kibana-plugin-public.handlerfunction.md) + +## HandlerFunction type + +A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) + +Signature: + +```typescript +export declare type HandlerFunction = (context: T, ...args: any[]) => any; +``` diff --git a/docs/development/core/public/kibana-plugin-public.handlerparameters.md b/docs/development/core/public/kibana-plugin-public.handlerparameters.md new file mode 100644 index 00000000000000..f46c4b649e9436 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.handlerparameters.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HandlerParameters](./kibana-plugin-public.handlerparameters.md) + +## HandlerParameters type + +Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). + +Signature: + +```typescript +export declare type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; +``` diff --git a/docs/development/core/public/kibana-plugin-public.icontextcontainer.createhandler.md b/docs/development/core/public/kibana-plugin-public.icontextcontainer.createhandler.md index 2a995df45757f3..af3b5e3fc2eb65 100644 --- a/docs/development/core/public/kibana-plugin-public.icontextcontainer.createhandler.md +++ b/docs/development/core/public/kibana-plugin-public.icontextcontainer.createhandler.md @@ -9,7 +9,7 @@ Create a new handler function pre-wired to context for the plugin. Signature: ```typescript -createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise; +createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; ``` ## Parameters @@ -17,11 +17,11 @@ createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandlerPluginOpaqueId | The plugin opaque ID for the plugin that registers this handler. | -| handler | IContextHandler<TContext, THandlerReturn, THandlerParameters> | Handler function to pass context object to. | +| handler | THandler | Handler function to pass context object to. | Returns: -`(...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise` +`(...rest: HandlerParameters) => ShallowPromise>` A function that takes `THandlerParameters`, calls `handler` with a new context, and returns a Promise of the `handler` return value. diff --git a/docs/development/core/public/kibana-plugin-public.icontextcontainer.md b/docs/development/core/public/kibana-plugin-public.icontextcontainer.md index 0bc7c8f3808d16..f16c07b3d7906d 100644 --- a/docs/development/core/public/kibana-plugin-public.icontextcontainer.md +++ b/docs/development/core/public/kibana-plugin-public.icontextcontainer.md @@ -9,7 +9,7 @@ An object that handles registration of context providers and configuring handler Signature: ```typescript -export interface IContextContainer +export interface IContextContainer> ``` ## Methods diff --git a/docs/development/core/public/kibana-plugin-public.icontextcontainer.registercontext.md b/docs/development/core/public/kibana-plugin-public.icontextcontainer.registercontext.md index 2cf10a6ec841d6..775f95bd7affaa 100644 --- a/docs/development/core/public/kibana-plugin-public.icontextcontainer.registercontext.md +++ b/docs/development/core/public/kibana-plugin-public.icontextcontainer.registercontext.md @@ -9,7 +9,7 @@ Register a new context provider. Signature: ```typescript -registerContext(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; +registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; ``` ## Parameters @@ -18,7 +18,7 @@ registerContext(pluginOpaqueId: PluginOpaqu | --- | --- | --- | | pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this context. | | contextName | TContextName | The key of the TContext object this provider supplies the value for. | -| provider | IContextProvider<TContext, TContextName, THandlerParameters> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) to be called each time a new context is created. | +| provider | IContextProvider<THandler, TContextName> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) to be called each time a new context is created. | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.icontexthandler.md b/docs/development/core/public/kibana-plugin-public.icontexthandler.md deleted file mode 100644 index 2251b1131c3138..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.icontexthandler.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IContextHandler](./kibana-plugin-public.icontexthandler.md) - -## IContextHandler type - -A function registered by a plugin to perform some action. - -Signature: - -```typescript -export declare type IContextHandler = (context: TContext, ...rest: THandlerParameters) => TReturn; -``` - -## Remarks - -A new `TContext` will be built for each handler before invoking. - diff --git a/docs/development/core/public/kibana-plugin-public.icontextprovider.md b/docs/development/core/public/kibana-plugin-public.icontextprovider.md index a84917d6e14420..40f0ee3782f6da 100644 --- a/docs/development/core/public/kibana-plugin-public.icontextprovider.md +++ b/docs/development/core/public/kibana-plugin-public.icontextprovider.md @@ -9,7 +9,7 @@ A function that returns a context value for a specific key of given context type Signature: ```typescript -export declare type IContextProvider, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; +export declare type IContextProvider, TContextName extends keyof HandlerContextType> = (context: Partial>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; ``` ## Remarks diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index e2ef807a75018c..7531cf9a063336 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -91,11 +91,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | +| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | +| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | +| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | | [HttpBody](./kibana-plugin-public.httpbody.md) | | | [HttpHandler](./kibana-plugin-public.httphandler.md) | | | [HttpSetup](./kibana-plugin-public.httpsetup.md) | | | [HttpStart](./kibana-plugin-public.httpstart.md) | | -| [IContextHandler](./kibana-plugin-public.icontexthandler.md) | A function registered by a plugin to perform some action. | | [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [OverlayBannerMount](./kibana-plugin-public.overlaybannermount.md) | A function that will mount the banner inside the provided element. | | [OverlayBannerUnmount](./kibana-plugin-public.overlaybannerunmount.md) | A function that will unmount the banner from the element. | diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 80ddb1aea18d15..a4fa3f17d0d94f 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 2ad9591426ab26..00a71d25cea38a 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "type" | "defaultSearchOperator" | "searchFields" | "sortField" | "hasReference" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md new file mode 100644 index 00000000000000..82237134e0b22c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.filter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) > [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md) + +## SavedObjectsFindOptions.filter property + +Signature: + +```typescript +filter?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md index f90c60ebdd0dc1..4c916431d4333f 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsfindoptions.md @@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-public.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-public.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | +| [filter](./kibana-plugin-public.savedobjectsfindoptions.filter.md) | string | | | [hasReference](./kibana-plugin-public.savedobjectsfindoptions.hasreference.md) | {
type: string;
id: string;
} | | | [page](./kibana-plugin-public.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-public.savedobjectsfindoptions.perpage.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-server.basepath.get.md b/docs/development/core/server/kibana-plugin-server.basepath.get.md new file mode 100644 index 00000000000000..04feca7ccc5a87 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [get](./kibana-plugin-server.basepath.get.md) + +## BasePath.get property + +returns `basePath` value, specific for an incoming request. + +Signature: + +```typescript +get: (request: LegacyRequest | KibanaRequest) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.md b/docs/development/core/server/kibana-plugin-server.basepath.md new file mode 100644 index 00000000000000..bfa1ea02aec178 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) + +## BasePath class + +Access or manipulate the Kibana base path + +Signature: + +```typescript +export declare class BasePath +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [get](./kibana-plugin-server.basepath.get.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown>) => string | returns basePath value, specific for an incoming request. | +| [prepend](./kibana-plugin-server.basepath.prepend.md) | | (path: string) => string | returns a new basePath value, prefixed with passed url. | +| [remove](./kibana-plugin-server.basepath.remove.md) | | (path: string) => string | returns a new basePath value, cleaned up from passed url. | +| [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request | +| [set](./kibana-plugin-server.basepath.set.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown>, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | + +## Remarks + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `BasePath` class. + diff --git a/docs/development/core/server/kibana-plugin-server.basepath.prepend.md b/docs/development/core/server/kibana-plugin-server.basepath.prepend.md new file mode 100644 index 00000000000000..113e8d9bf48803 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.prepend.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [prepend](./kibana-plugin-server.basepath.prepend.md) + +## BasePath.prepend property + +returns a new `basePath` value, prefixed with passed `url`. + +Signature: + +```typescript +prepend: (path: string) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.remove.md b/docs/development/core/server/kibana-plugin-server.basepath.remove.md new file mode 100644 index 00000000000000..c5f1092d2969d9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.remove.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [remove](./kibana-plugin-server.basepath.remove.md) + +## BasePath.remove property + +returns a new `basePath` value, cleaned up from passed `url`. + +Signature: + +```typescript +remove: (path: string) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.serverbasepath.md b/docs/development/core/server/kibana-plugin-server.basepath.serverbasepath.md new file mode 100644 index 00000000000000..d7e45a92dba6d7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.serverbasepath.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) + +## BasePath.serverBasePath property + +returns the server's basePath + +See [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request + +Signature: + +```typescript +readonly serverBasePath: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.set.md b/docs/development/core/server/kibana-plugin-server.basepath.set.md new file mode 100644 index 00000000000000..cec70ee853bfa4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.basepath.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [set](./kibana-plugin-server.basepath.set.md) + +## BasePath.set property + +sets `basePath` value, specific for an incoming request. + +Signature: + +```typescript +set: (request: LegacyRequest | KibanaRequest, requestSpecificBasePath: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md b/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md index f44e6a3d7640b7..7096bfc43a520b 100644 --- a/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md +++ b/docs/development/core/server/kibana-plugin-server.contextsetup.createcontextcontainer.md @@ -9,9 +9,9 @@ Creates a new [IContextContainer](./kibana-plugin-server.icontextcontainer.md) f Signature: ```typescript -createContextContainer(): IContextContainer; +createContextContainer>(): IContextContainer; ``` Returns: -`IContextContainer` +`IContextContainer` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index 254f2728abef16..8474f4ef940dc6 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -14,7 +14,7 @@ http: { registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; - registerRouteHandlerContext: (name: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; + registerRouteHandlerContext: (name: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; createRouter: () => IRouter; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index ed487a570f2863..528557e91bd17f 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -18,5 +18,5 @@ export interface CoreSetup | --- | --- | --- | | [context](./kibana-plugin-server.coresetup.context.md) | {
createContextContainer: ContextSetup['createContextContainer'];
} | | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;
} | | -| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
registerRouteHandlerContext: <T extends keyof RequestHandlerContext>(name: T, provider: RequestHandlerContextProvider<RequestHandlerContext>) => RequestHandlerContextContainer<RequestHandlerContext>;
createRouter: () => IRouter;
} | | +| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
registerRouteHandlerContext: <T extends keyof RequestHandlerContext>(name: T, provider: RequestHandlerContextProvider<T>) => RequestHandlerContextContainer;
createRouter: () => IRouter;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.handlercontexttype.md b/docs/development/core/server/kibana-plugin-server.handlercontexttype.md new file mode 100644 index 00000000000000..e8f1f346e8b8e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.handlercontexttype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HandlerContextType](./kibana-plugin-server.handlercontexttype.md) + +## HandlerContextType type + +Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-server.handlerfunction.md) to represent the type of the context. + +Signature: + +```typescript +export declare type HandlerContextType> = T extends HandlerFunction ? U : never; +``` diff --git a/docs/development/core/server/kibana-plugin-server.handlerfunction.md b/docs/development/core/server/kibana-plugin-server.handlerfunction.md new file mode 100644 index 00000000000000..97acd37946fc94 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.handlerfunction.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HandlerFunction](./kibana-plugin-server.handlerfunction.md) + +## HandlerFunction type + +A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-server.icontextcontainer.md) + +Signature: + +```typescript +export declare type HandlerFunction = (context: T, ...args: any[]) => any; +``` diff --git a/docs/development/core/server/kibana-plugin-server.handlerparameters.md b/docs/development/core/server/kibana-plugin-server.handlerparameters.md new file mode 100644 index 00000000000000..3dd7998a71a1f2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.handlerparameters.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HandlerParameters](./kibana-plugin-server.handlerparameters.md) + +## HandlerParameters type + +Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-server.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-server.handlercontexttype.md). + +Signature: + +```typescript +export declare type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md index 5cfb2f5c4e8b43..173262de10494f 100644 --- a/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.basepath.md @@ -4,13 +4,10 @@ ## HttpServerSetup.basePath property +[BasePath](./kibana-plugin-server.basepath.md) + Signature: ```typescript -basePath: { - get: (request: KibanaRequest | LegacyRequest) => string; - set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; - prepend: (url: string) => string; - remove: (url: string) => string; - }; +basePath: IBasePath; ``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.md index f495de850aff5e..7a126383116e7b 100644 --- a/docs/development/core/server/kibana-plugin-server.httpserversetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.md @@ -17,7 +17,7 @@ export interface HttpServerSetup | Property | Type | Description | | --- | --- | --- | | [auth](./kibana-plugin-server.httpserversetup.auth.md) | {
get: GetAuthState;
isAuthenticated: IsAuthenticated;
getAuthHeaders: GetAuthHeaders;
} | | -| [basePath](./kibana-plugin-server.httpserversetup.basepath.md) | {
get: (request: KibanaRequest | LegacyRequest) => string;
set: (request: KibanaRequest | LegacyRequest, basePath: string) => void;
prepend: (url: string) => string;
remove: (url: string) => string;
} | | +| [basePath](./kibana-plugin-server.httpserversetup.basepath.md) | IBasePath | [BasePath](./kibana-plugin-server.basepath.md) | | [createCookieSessionStorageFactory](./kibana-plugin-server.httpserversetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | | [isTlsEnabled](./kibana-plugin-server.httpserversetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | | [registerAuth](./kibana-plugin-server.httpserversetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. A handler should return a state to associate with the incoming request. The state can be retrieved later via http.auth.get(..) Only one AuthenticationHandler can be registered. | diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 92bf158ad33129..eec63cf5c80931 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -10,6 +10,6 @@ ```typescript export declare type HttpServiceSetup = Omit & { createRouter: (path: string, plugin?: PluginOpaqueId) => IRouter; - registerRouteHandlerContext: (pluginOpaqueId: PluginOpaqueId, contextName: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; + registerRouteHandlerContext: (pluginOpaqueId: PluginOpaqueId, contextName: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.ibasepath.md b/docs/development/core/server/kibana-plugin-server.ibasepath.md new file mode 100644 index 00000000000000..2baa8d623ce97b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.ibasepath.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IBasePath](./kibana-plugin-server.ibasepath.md) + +## IBasePath type + +Access or manipulate the Kibana base path + +[BasePath](./kibana-plugin-server.basepath.md) + +Signature: + +```typescript +export declare type IBasePath = Pick; +``` diff --git a/docs/development/core/server/kibana-plugin-server.icontextcontainer.createhandler.md b/docs/development/core/server/kibana-plugin-server.icontextcontainer.createhandler.md index c5549ab017e530..09a9e28d6d0fe8 100644 --- a/docs/development/core/server/kibana-plugin-server.icontextcontainer.createhandler.md +++ b/docs/development/core/server/kibana-plugin-server.icontextcontainer.createhandler.md @@ -9,7 +9,7 @@ Create a new handler function pre-wired to context for the plugin. Signature: ```typescript -createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise; +createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; ``` ## Parameters @@ -17,11 +17,11 @@ createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandlerPluginOpaqueId | The plugin opaque ID for the plugin that registers this handler. | -| handler | IContextHandler<TContext, THandlerReturn, THandlerParameters> | Handler function to pass context object to. | +| handler | THandler | Handler function to pass context object to. | Returns: -`(...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise` +`(...rest: HandlerParameters) => ShallowPromise>` A function that takes `THandlerParameters`, calls `handler` with a new context, and returns a Promise of the `handler` return value. diff --git a/docs/development/core/server/kibana-plugin-server.icontextcontainer.md b/docs/development/core/server/kibana-plugin-server.icontextcontainer.md index 1ab699be105b4b..114da31442ff9b 100644 --- a/docs/development/core/server/kibana-plugin-server.icontextcontainer.md +++ b/docs/development/core/server/kibana-plugin-server.icontextcontainer.md @@ -9,7 +9,7 @@ An object that handles registration of context providers and configuring handler Signature: ```typescript -export interface IContextContainer +export interface IContextContainer> ``` ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.icontextcontainer.registercontext.md b/docs/development/core/server/kibana-plugin-server.icontextcontainer.registercontext.md index 1a63f63dc91b46..30d3fc154d1e93 100644 --- a/docs/development/core/server/kibana-plugin-server.icontextcontainer.registercontext.md +++ b/docs/development/core/server/kibana-plugin-server.icontextcontainer.registercontext.md @@ -9,7 +9,7 @@ Register a new context provider. Signature: ```typescript -registerContext(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; +registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; ``` ## Parameters @@ -18,7 +18,7 @@ registerContext(pluginOpaqueId: PluginOpaqu | --- | --- | --- | | pluginOpaqueId | PluginOpaqueId | The plugin opaque ID for the plugin that registers this context. | | contextName | TContextName | The key of the TContext object this provider supplies the value for. | -| provider | IContextProvider<TContext, TContextName, THandlerParameters> | A [IContextProvider](./kibana-plugin-server.icontextprovider.md) to be called each time a new context is created. | +| provider | IContextProvider<THandler, TContextName> | A [IContextProvider](./kibana-plugin-server.icontextprovider.md) to be called each time a new context is created. | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.icontexthandler.md b/docs/development/core/server/kibana-plugin-server.icontexthandler.md deleted file mode 100644 index c1f5acc22734aa..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.icontexthandler.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IContextHandler](./kibana-plugin-server.icontexthandler.md) - -## IContextHandler type - -A function registered by a plugin to perform some action. - -Signature: - -```typescript -export declare type IContextHandler = (context: TContext, ...rest: THandlerParameters) => TReturn; -``` - -## Remarks - -A new `TContext` will be built for each handler before invoking. - diff --git a/docs/development/core/server/kibana-plugin-server.icontextprovider.md b/docs/development/core/server/kibana-plugin-server.icontextprovider.md index 250e6a2be3f6ae..39ace8b9bc57ee 100644 --- a/docs/development/core/server/kibana-plugin-server.icontextprovider.md +++ b/docs/development/core/server/kibana-plugin-server.icontextprovider.md @@ -9,7 +9,7 @@ A function that returns a context value for a specific key of given context type Signature: ```typescript -export declare type IContextProvider, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; +export declare type IContextProvider, TContextName extends keyof HandlerContextType> = (context: Partial>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-server.internalcorestart.md b/docs/development/core/server/kibana-plugin-server.internalcorestart.md deleted file mode 100644 index 4943249d284b04..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.internalcorestart.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) - -## InternalCoreStart interface - - -Signature: - -```typescript -export interface InternalCoreStart -``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index baecb180096de9..3c01e7aeef325b 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -16,12 +16,11 @@ The plugin integrates with the core system via lifecycle events: `setup` | Class | Description | | --- | --- | +| [BasePath](./kibana-plugin-server.basepath.md) | Access or manipulate the Kibana base path | | [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(...)). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | -| [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) | | -| [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.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 | ## Enumerations @@ -51,7 +50,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [IContextContainer](./kibana-plugin-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | -| [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | | [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | @@ -95,7 +93,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsMigrationVersion](./kibana-plugin-server.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [SavedObjectsRawDoc](./kibana-plugin-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. | | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | -| [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. | @@ -119,10 +116,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | | [GetAuthState](./kibana-plugin-server.getauthstate.md) | Get authentication state for a request. Returned by auth interceptor. | +| [HandlerContextType](./kibana-plugin-server.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-server.handlerfunction.md) to represent the type of the context. | +| [HandlerFunction](./kibana-plugin-server.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-server.icontextcontainer.md) | +| [HandlerParameters](./kibana-plugin-server.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-server.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-server.handlercontexttype.md). | | [Headers](./kibana-plugin-server.headers.md) | Http request headers to read. | | [HttpResponsePayload](./kibana-plugin-server.httpresponsepayload.md) | Data send to the client as a response payload. | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | | -| [IContextHandler](./kibana-plugin-server.icontexthandler.md) | A function registered by a plugin to perform some action. | +| [IBasePath](./kibana-plugin-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-server.basepath.md) | | [IContextProvider](./kibana-plugin-server.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IsAuthenticated](./kibana-plugin-server.isauthenticated.md) | Return authentication status for a request. | | [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | @@ -138,8 +138,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RequestHandler](./kibana-plugin-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-server.kibanaresponsefactory.md) functions. | | [RequestHandlerContextContainer](./kibana-plugin-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. | | [RequestHandlerContextProvider](./kibana-plugin-server.requesthandlercontextprovider.md) | Context provider for request handler. Extends request context object with provided functionality or data. | -| [RequestHandlerParams](./kibana-plugin-server.requesthandlerparams.md) | Parameters passed to the request handler function. | -| [RequestHandlerReturn](./kibana-plugin-server.requesthandlerreturn.md) | Expected outcome the request handler function. | | [ResponseError](./kibana-plugin-server.responseerror.md) | Error message and optional data send to the client in case of error. | | [ResponseErrorAttributes](./kibana-plugin-server.responseerrorattributes.md) | Additional data to provide error details. | | [ResponseHeaders](./kibana-plugin-server.responseheaders.md) | Http response headers to set. | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontextcontainer.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontextcontainer.md index afdb4885970696..b76a9ce7d235ca 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontextcontainer.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontextcontainer.md @@ -9,5 +9,5 @@ An object that handles registration of http request context providers. Signature: ```typescript -export declare type RequestHandlerContextContainer = IContextContainer, RequestHandlerParams>; +export declare type RequestHandlerContextContainer = IContextContainer>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontextprovider.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontextprovider.md index 0d9cc6b70b80c0..ea7294b721aabd 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontextprovider.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontextprovider.md @@ -9,5 +9,5 @@ Context provider for request handler. Extends request context object with provid Signature: ```typescript -export declare type RequestHandlerContextProvider = IContextProvider; +export declare type RequestHandlerContextProvider = IContextProvider, TContextName>; ``` diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlerparams.md b/docs/development/core/server/kibana-plugin-server.requesthandlerparams.md deleted file mode 100644 index 7f466845b4d460..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.requesthandlerparams.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RequestHandlerParams](./kibana-plugin-server.requesthandlerparams.md) - -## RequestHandlerParams type - -Parameters passed to the request handler function. - -Signature: - -```typescript -export declare type RequestHandlerParams = [KibanaRequest, KibanaResponseFactory]; -``` diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlerreturn.md b/docs/development/core/server/kibana-plugin-server.requesthandlerreturn.md deleted file mode 100644 index 6c01e21b6ecbe9..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.requesthandlerreturn.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RequestHandlerReturn](./kibana-plugin-server.requesthandlerreturn.md) - -## RequestHandlerReturn type - -Expected outcome the request handler function. - -Signature: - -```typescript -export declare type RequestHandlerReturn = KibanaResponse; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md new file mode 100644 index 00000000000000..308bebbeaf60b8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.filter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) > [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.md) + +## SavedObjectsFindOptions.filter property + +Signature: + +```typescript +filter?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md index ad81c439d902c0..dfd51d480db926 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfindoptions.md @@ -17,6 +17,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions | --- | --- | --- | | [defaultSearchOperator](./kibana-plugin-server.savedobjectsfindoptions.defaultsearchoperator.md) | 'AND' | 'OR' | | | [fields](./kibana-plugin-server.savedobjectsfindoptions.fields.md) | string[] | An array of fields to include in the results | +| [filter](./kibana-plugin-server.savedobjectsfindoptions.filter.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 | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema._constructor_.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema._constructor_.md deleted file mode 100644 index f4fb88fa6d4f11..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [(constructor)](./kibana-plugin-server.savedobjectsschema._constructor_.md) - -## SavedObjectsSchema.(constructor) - -Constructs a new instance of the `SavedObjectsSchema` class - -Signature: - -```typescript -constructor(schemaDefinition?: SavedObjectsSchemaDefinition); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| schemaDefinition | SavedObjectsSchemaDefinition | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md deleted file mode 100644 index 5baf075463558a..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [getConvertToAliasScript](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) - -## SavedObjectsSchema.getConvertToAliasScript() method - -Signature: - -```typescript -getConvertToAliasScript(type: string): string | undefined; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | - -Returns: - -`string | undefined` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md deleted file mode 100644 index ba1c439c8c6b4e..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.getindexfortype.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [getIndexForType](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) - -## SavedObjectsSchema.getIndexForType() method - -Signature: - -```typescript -getIndexForType(config: Config, type: string): string | undefined; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| config | Config | | -| type | string | | - -Returns: - -`string | undefined` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md deleted file mode 100644 index f67b12a4d14c3d..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.ishiddentype.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [isHiddenType](./kibana-plugin-server.savedobjectsschema.ishiddentype.md) - -## SavedObjectsSchema.isHiddenType() method - -Signature: - -```typescript -isHiddenType(type: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md deleted file mode 100644 index 2ca0abd7e4aa7e..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) > [isNamespaceAgnostic](./kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md) - -## SavedObjectsSchema.isNamespaceAgnostic() method - -Signature: - -```typescript -isNamespaceAgnostic(type: string): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| type | string | | - -Returns: - -`boolean` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md b/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md deleted file mode 100644 index 0808811804eaf5..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsschema.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) - -## SavedObjectsSchema class - -Signature: - -```typescript -export declare class SavedObjectsSchema -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(schemaDefinition)](./kibana-plugin-server.savedobjectsschema._constructor_.md) | | Constructs a new instance of the SavedObjectsSchema class | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [getConvertToAliasScript(type)](./kibana-plugin-server.savedobjectsschema.getconverttoaliasscript.md) | | | -| [getIndexForType(config, type)](./kibana-plugin-server.savedobjectsschema.getindexfortype.md) | | | -| [isHiddenType(type)](./kibana-plugin-server.savedobjectsschema.ishiddentype.md) | | | -| [isNamespaceAgnostic(type)](./kibana-plugin-server.savedobjectsschema.isnamespaceagnostic.md) | | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer._constructor_.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer._constructor_.md deleted file mode 100644 index c05e97d3dbcdf8..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [(constructor)](./kibana-plugin-server.savedobjectsserializer._constructor_.md) - -## SavedObjectsSerializer.(constructor) - -Constructs a new instance of the `SavedObjectsSerializer` class - -Signature: - -```typescript -constructor(schema: SavedObjectsSchema); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| schema | SavedObjectsSchema | | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md deleted file mode 100644 index 4705f48a201aee..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [generateRawId](./kibana-plugin-server.savedobjectsserializer.generaterawid.md) - -## SavedObjectsSerializer.generateRawId() method - -Given a saved object type and id, generates the compound id that is stored in the raw document. - -Signature: - -```typescript -generateRawId(namespace: string | undefined, type: string, id?: string): string; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| namespace | string | undefined | | -| type | string | | -| id | string | | - -Returns: - -`string` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md deleted file mode 100644 index e190e7bce8c011..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [isRawSavedObject](./kibana-plugin-server.savedobjectsserializer.israwsavedobject.md) - -## SavedObjectsSerializer.isRawSavedObject() method - -Determines whether or not the raw document can be converted to a saved object. - -Signature: - -```typescript -isRawSavedObject(rawDoc: RawDoc): any; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| rawDoc | RawDoc | | - -Returns: - -`any` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md deleted file mode 100644 index dd3f52554a81ea..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) - -## SavedObjectsSerializer class - -Signature: - -```typescript -export declare class SavedObjectsSerializer -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(schema)](./kibana-plugin-server.savedobjectsserializer._constructor_.md) | | Constructs a new instance of the SavedObjectsSerializer class | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [generateRawId(namespace, type, id)](./kibana-plugin-server.savedobjectsserializer.generaterawid.md) | | Given a saved object type and id, generates the compound id that is stored in the raw document. | -| [isRawSavedObject(rawDoc)](./kibana-plugin-server.savedobjectsserializer.israwsavedobject.md) | | Determines whether or not the raw document can be converted to a saved object. | -| [rawToSavedObject(doc)](./kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md) | | Converts a document from the format that is stored in elasticsearch to the saved object client format. | -| [savedObjectToRaw(savedObj)](./kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md) | | Converts a document from the saved object client format to the format that is stored in elasticsearch. | - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md deleted file mode 100644 index b36cdb3be64da9..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [rawToSavedObject](./kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md) - -## SavedObjectsSerializer.rawToSavedObject() method - -Converts a document from the format that is stored in elasticsearch to the saved object client format. - -Signature: - -```typescript -rawToSavedObject(doc: RawDoc): SanitizedSavedObjectDoc; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| doc | RawDoc | | - -Returns: - -`SanitizedSavedObjectDoc` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md deleted file mode 100644 index 4854a97a845b89..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) > [savedObjectToRaw](./kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md) - -## SavedObjectsSerializer.savedObjectToRaw() method - -Converts a document from the saved object client format to the format that is stored in elasticsearch. - -Signature: - -```typescript -savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): RawDoc; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| savedObj | SanitizedSavedObjectDoc | | - -Returns: - -`RawDoc` - diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md deleted file mode 100644 index 6e0d1a827750cf..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.addscopedsavedobjectsclientwrapperfactory.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[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 deleted file mode 100644 index 13ccad7ed01ae5..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getsavedobjectsrepository.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[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 deleted file mode 100644 index c762de041edf5f..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.getscopedsavedobjectsclient.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[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.importexport.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md deleted file mode 100644 index f9b4e46712f4a1..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.importexport.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [importExport](./kibana-plugin-server.savedobjectsservice.importexport.md) - -## SavedObjectsService.importExport property - -Signature: - -```typescript -importExport: { - objectLimit: number; - importSavedObjects(options: SavedObjectsImportOptions): Promise; - resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; - getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; - }; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md deleted file mode 100644 index d9e23e6f15928e..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.md +++ /dev/null @@ -1,30 +0,0 @@ - - -[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'] | | -| [importExport](./kibana-plugin-server.savedobjectsservice.importexport.md) | {
objectLimit: number;
importSavedObjects(options: SavedObjectsImportOptions): Promise<SavedObjectsImportResponse>;
resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise<SavedObjectsImportResponse>;
getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise<Readable>;
} | | -| [SavedObjectsClient](./kibana-plugin-server.savedobjectsservice.savedobjectsclient.md) | typeof SavedObjectsClient | | -| [schema](./kibana-plugin-server.savedobjectsservice.schema.md) | SavedObjectsSchema | | -| [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 deleted file mode 100644 index 4a7722928e85e0..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.savedobjectsclient.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[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.schema.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md deleted file mode 100644 index be5682e6f034e4..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.schema.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsService](./kibana-plugin-server.savedobjectsservice.md) > [schema](./kibana-plugin-server.savedobjectsservice.schema.md) - -## SavedObjectsService.schema property - -Signature: - -```typescript -schema: SavedObjectsSchema; -``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md deleted file mode 100644 index a783ef4270f186..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservice.types.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[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/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 25c07dc5024840..8d9ef515108eda 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -35,8 +35,11 @@ image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollu {kib} makes it easy for you to create an index pattern by walking you through the process. Just start typing in the *Index pattern* field, and {kib} looks for -the names of {es} indices that match your input. If you want to include -system indices in your search, toggle the switch in the upper right. +the names of {es} indices that match your input. Make sure that the name of the +index pattern is unique. + +If you want to include system indices in your search, toggle the switch in the +upper right. [role="screenshot"] image:management/index-patterns/images/create-index-pattern.png["Create index pattern"] diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 7330c7e144b60e..97fb891c95bdba 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -21,7 +21,7 @@ To control how data is collected from your {es} nodes, you configure {ref}/monitoring-settings.html[`xpack.monitoring.collection` settings] in `elasticsearch.yml`. To control how monitoring data is collected from Logstash, you configure -{logstash-ref}/configuring-logstash.html#monitoring-settings[`xpack.monitoring` settings] +{logstash-ref}/monitoring-internal-collection.html#monitoring-settings[`xpack.monitoring` settings] in `logstash.yml`. For more information, see diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 7f9034c48e232f..5b3db22a39ea64 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -256,10 +256,6 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). `server.customResponseHeaders:`:: *Default: `{}`* Header names and values to send on all responses to the client from the Kibana server. -[[server-default]]`server.defaultRoute:`:: *Default: "/app/kibana"* This setting -specifies the default route when opening Kibana. You can use this setting to -modify the landing page when opening Kibana. Supported on {ece}. - `server.host:`:: *Default: "localhost"* This setting specifies the host of the back end server. diff --git a/kibana.d.ts b/kibana.d.ts index e0b20f6fa28af6..d242965e9bdd5a 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -42,7 +42,7 @@ export namespace Legacy { export type Request = LegacyKibanaServer.Request; export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit; export type SavedObjectsClient = LegacyKibanaServer.SavedObjectsClient; - export type SavedObjectsService = LegacyKibanaServer.SavedObjectsService; + export type SavedObjectsService = LegacyKibanaServer.SavedObjectsLegacyService; export type Server = LegacyKibanaServer.Server; export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction; diff --git a/package.json b/package.json index b8aca7cf2c4092..e60819102eb918 100644 --- a/package.json +++ b/package.json @@ -105,9 +105,9 @@ "dependencies": { "@babel/core": "^7.5.5", "@babel/register": "^7.5.5", - "@elastic/charts": "^12.0.2", + "@elastic/charts": "^12.1.0", "@elastic/datemath": "5.0.2", - "@elastic/eui": "14.3.0", + "@elastic/eui": "14.4.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -160,7 +160,6 @@ "expiry-js": "0.1.7", "file-loader": "4.2.0", "font-awesome": "4.7.0", - "fp-ts": "^2.0.5", "getos": "^3.1.0", "glob": "^7.1.2", "glob-all": "^3.1.0", @@ -177,7 +176,6 @@ "https-proxy-agent": "^2.2.2", "inert": "^5.1.0", "inline-style": "^2.0.0", - "io-ts": "^2.0.1", "joi": "^13.5.2", "jquery": "^3.4.1", "js-yaml": "3.13.1", @@ -242,7 +240,7 @@ "style-loader": "0.23.1", "symbol-observable": "^1.2.0", "tar": "4.4.13", - "terser-webpack-plugin": "^1.4.1", + "terser-webpack-plugin": "^2.1.2", "thread-loader": "^2.1.3", "tinygradient": "0.4.3", "tinymath": "1.2.1", @@ -375,10 +373,11 @@ "eslint-config-prettier": "6.3.0", "eslint-plugin-babel": "^5.3.0", "eslint-plugin-ban": "1.3.0", + "eslint-plugin-cypress": "^2.6.1", "eslint-plugin-import": "2.18.2", "eslint-plugin-jest": "22.17.0", "eslint-plugin-jsx-a11y": "6.2.3", - "eslint-plugin-mocha": "5.3.0", + "eslint-plugin-mocha": "6.1.1", "eslint-plugin-no-unsanitized": "3.0.2", "eslint-plugin-node": "9.2.0", "eslint-plugin-prefer-object-spread": "1.2.1", @@ -421,7 +420,7 @@ "license-checker": "^16.0.0", "listr": "^0.14.1", "load-grunt-config": "^3.0.1", - "mocha": "3.5.3", + "mocha": "6.2.1", "multistream": "^2.1.1", "murmurhash3js": "3.0.1", "mutation-observer": "^1.0.3", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 8c6359e66a7a5d..da2a37cc41ad3c 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -24,7 +24,7 @@ "eslint-plugin-jsx-a11y": "6.2.3", "eslint-plugin-import": "2.18.2", "eslint-plugin-jest": "^22.17.0", - "eslint-plugin-mocha": "^5.3.0", + "eslint-plugin-mocha": "^6.1.1", "eslint-plugin-no-unsanitized": "3.0.2", "eslint-plugin-prefer-object-spread": "1.2.1", "eslint-plugin-react": "7.13.0", diff --git a/packages/kbn-babel-code-parser/src/can_require.js b/packages/kbn-babel-code-parser/src/can_require.js index e590c249e9806f..4d85910abe6ed6 100644 --- a/packages/kbn-babel-code-parser/src/can_require.js +++ b/packages/kbn-babel-code-parser/src/can_require.js @@ -17,18 +17,18 @@ * under the License. */ -export function canRequire(cwd, entry) { +export function canRequire(entry, cwd = require.resolve.paths(entry) || []) { try { // We will try to test if we can resolve // this entry through the require.resolve // setting as the start looking path the - // given cwd. Require.resolve will keep + // given cwd. That cwd variable could be + // a path or an array of paths + // from where Require.resolve will keep // looking recursively as normal starting - // from that location. + // from those locations. return require.resolve(entry, { - paths: [ - cwd - ] + paths: [].concat(cwd) }); } catch (e) { return false; diff --git a/packages/kbn-babel-code-parser/src/code_parser.js b/packages/kbn-babel-code-parser/src/code_parser.js index 8d76b1032561ac..0f53bd249bb5cc 100644 --- a/packages/kbn-babel-code-parser/src/code_parser.js +++ b/packages/kbn-babel-code-parser/src/code_parser.js @@ -79,7 +79,7 @@ export async function parseEntries(cwd, entries, strategy, results, wasParsed = const sanitizedCwd = cwd || process.cwd(); // Test each entry against canRequire function - const entriesQueue = entries.map(entry => canRequire(sanitizedCwd, entry)); + const entriesQueue = entries.map(entry => canRequire(entry)); while(entriesQueue.length) { // Get the first element in the queue as diff --git a/packages/kbn-babel-code-parser/src/strategies.js b/packages/kbn-babel-code-parser/src/strategies.js index 317ded014210b1..89621bc53bd534 100644 --- a/packages/kbn-babel-code-parser/src/strategies.js +++ b/packages/kbn-babel-code-parser/src/strategies.js @@ -62,8 +62,12 @@ export async function dependenciesParseStrategy(cwd, parseSingleFile, mainEntry, // new dependencies return dependencies.reduce((filteredEntries, entry) => { const absEntryPath = resolve(cwd, dirname(mainEntry), entry); - const requiredPath = canRequire(cwd, absEntryPath); - const requiredRelativePath = canRequire(cwd, entry); + + // NOTE: cwd for following canRequires is absEntryPath + // because we should start looking from there + const requiredPath = canRequire(absEntryPath, absEntryPath); + const requiredRelativePath = canRequire(entry, absEntryPath); + const isRelativeFile = !isAbsolute(entry); const isNodeModuleDep = isRelativeFile && !requiredPath && requiredRelativePath; const isNewEntry = isRelativeFile && requiredPath; diff --git a/packages/kbn-babel-code-parser/src/strategies.test.js b/packages/kbn-babel-code-parser/src/strategies.test.js index 5a84edf560af13..d7caa8b95d4a22 100644 --- a/packages/kbn-babel-code-parser/src/strategies.test.js +++ b/packages/kbn-babel-code-parser/src/strategies.test.js @@ -59,8 +59,8 @@ describe('Code Parser Strategies', () => { cb(null, `require('dep_from_node_modules')`); }); - canRequire.mockImplementation((mockCwd, entry) => { - if (entry === `${mockCwd}dep1/dep_from_node_modules`) { + canRequire.mockImplementation((entry, cwd) => { + if (entry === `${cwd}dep1/dep_from_node_modules`) { return false; } @@ -78,7 +78,7 @@ describe('Code Parser Strategies', () => { cb(null, `require('./relative_dep')`); }); - canRequire.mockImplementation((mockCwd, entry) => { + canRequire.mockImplementation((entry) => { if (entry === `${mockCwd}dep1/relative_dep`) { return `${entry}/index.js`; } diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 6d3914eb56218b..5c69036a4b13ae 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -23,3 +23,4 @@ export { createAbsolutePathSerializer } from './serializers'; export { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs'; export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './constants'; +export { KbnClient } from './kbn_client'; diff --git a/packages/kbn-dev-utils/src/kbn_client/errors.ts b/packages/kbn-dev-utils/src/kbn_client/errors.ts new file mode 100644 index 00000000000000..068c68555b62ae --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/errors.ts @@ -0,0 +1,42 @@ +/* + * 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 { AxiosError, AxiosResponse } from 'axios'; + +export interface AxiosRequestError extends AxiosError { + response: undefined; +} + +export interface AxiosResponseError extends AxiosError { + response: AxiosResponse; +} + +export const isAxiosRequestError = (error: any): error is AxiosRequestError => { + return error && error.code === undefined && error.response === undefined; +}; + +export const isAxiosResponseError = (error: any): error is AxiosResponseError => { + return error && error.code !== undefined && error.response !== undefined; +}; + +export const isConcliftOnGetError = (error: any) => { + return ( + isAxiosResponseError(error) && error.config.method === 'GET' && error.response.status === 409 + ); +}; diff --git a/src/legacy/ui/ui_settings/__tests__/lib/index.js b/packages/kbn-dev-utils/src/kbn_client/index.ts similarity index 88% rename from src/legacy/ui/ui_settings/__tests__/lib/index.js rename to packages/kbn-dev-utils/src/kbn_client/index.ts index 29b1adbcba5760..72214b6c617462 100644 --- a/src/legacy/ui/ui_settings/__tests__/lib/index.js +++ b/packages/kbn-dev-utils/src/kbn_client/index.ts @@ -17,7 +17,5 @@ * under the License. */ -export { - createObjectsClientStub, - savedObjectsClientErrors, -} from './create_objects_client_stub'; +export { KbnClient } from './kbn_client'; +export { uriencode } from './kbn_client_requester'; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts new file mode 100644 index 00000000000000..2eb6c6cc5aac6b --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client.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 { ToolingLog } from '../tooling_log'; +import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; +import { KbnClientStatus } from './kbn_client_status'; +import { KbnClientPlugins } from './kbn_client_plugins'; +import { KbnClientVersion } from './kbn_client_version'; +import { KbnClientSavedObjects } from './kbn_client_saved_objects'; +import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; + +export class KbnClient { + private readonly requester = new KbnClientRequester(this.log, this.kibanaUrls); + readonly status = new KbnClientStatus(this.requester); + readonly plugins = new KbnClientPlugins(this.status); + readonly version = new KbnClientVersion(this.status); + readonly savedObjects = new KbnClientSavedObjects(this.log, this.requester); + readonly uiSettings = new KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); + + /** + * Basic Kibana server client that implements common behaviors for talking + * to the Kibana server from dev tooling. + * + * @param log ToolingLog + * @param kibanaUrls Array of kibana server urls to send requests to + * @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets + */ + constructor( + private readonly log: ToolingLog, + private readonly kibanaUrls: string[], + private readonly uiSettingDefaults?: UiSettingValues + ) { + if (!kibanaUrls.length) { + throw new Error('missing Kibana urls'); + } + } + + /** + * Make a direct request to the Kibana server + */ + async request(options: ReqOptions) { + return await this.requester.request(options); + } + + resolveUrl(relativeUrl: string) { + return this.requester.resolveUrl(relativeUrl); + } +} diff --git a/test/common/services/kibana_server/version.js b/packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts similarity index 57% rename from test/common/services/kibana_server/version.js rename to packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts index b7efb01c63449a..80285caf365a0b 100644 --- a/test/common/services/kibana_server/version.js +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_plugins.ts @@ -17,23 +17,28 @@ * under the License. */ -export class KibanaServerVersion { - constructor(kibanaStatus) { - this.kibanaStatus = kibanaStatus; - this._cachedVersionNumber; - } +import { KbnClientStatus } from './kbn_client_status'; - async get() { - if (this._cachedVersionNumber) { - return this._cachedVersionNumber; - } +const PLUGIN_STATUS_ID = /^plugin:(.+?)@/; + +export class KbnClientPlugins { + constructor(private readonly status: KbnClientStatus) {} + /** + * Get a list of plugin ids that are enabled on the server + */ + public async getEnabledIds() { + const pluginIds: string[] = []; + const apiResp = await this.status.get(); - const status = await this.kibanaStatus.get(); - if (status && status.version && status.version.number) { - this._cachedVersionNumber = status.version.number + (status.version.build_snapshot ? '-SNAPSHOT' : ''); - return this._cachedVersionNumber; + for (const status of apiResp.status.statuses) { + if (status.id) { + const match = status.id.match(PLUGIN_STATUS_ID); + if (match) { + pluginIds.push(match[1]); + } + } } - throw new Error(`Unable to fetch Kibana Server status, received ${JSON.stringify(status)}`); + return pluginIds; } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts new file mode 100644 index 00000000000000..56d4d7f99e0b80 --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Url from 'url'; + +import Axios from 'axios'; + +import { isAxiosRequestError, isConcliftOnGetError } from './errors'; +import { ToolingLog } from '../tooling_log'; + +export const uriencode = ( + strings: TemplateStringsArray, + ...values: Array +) => { + const queue = strings.slice(); + + if (queue.length === 0) { + throw new Error('how could strings passed to `uriencode` template tag be empty?'); + } + + if (queue.length !== values.length + 1) { + throw new Error('strings and values passed to `uriencode` template tag are unbalanced'); + } + + // pull the first string off the queue, there is one less item in `values` + // since the values are always wrapped in strings, so we shift the extra string + // off the queue to balance the queue and values array. + const leadingString = queue.shift()!; + return queue.reduce( + (acc, string, i) => `${acc}${encodeURIComponent(values[i])}${string}`, + leadingString + ); +}; + +const DEFAULT_MAX_ATTEMPTS = 5; + +export interface ReqOptions { + description?: string; + path: string; + query?: Record; + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + body?: any; + attempt?: number; + maxAttempts?: number; +} + +const delay = (ms: number) => + new Promise(resolve => { + setTimeout(resolve, ms); + }); + +export class KbnClientRequester { + constructor(private readonly log: ToolingLog, private readonly kibanaUrls: string[]) {} + + private pickUrl() { + const url = this.kibanaUrls.shift()!; + this.kibanaUrls.push(url); + return url; + } + + public resolveUrl(relativeUrl: string = '/') { + return Url.resolve(this.pickUrl(), relativeUrl); + } + + async request(options: ReqOptions): Promise { + const url = Url.resolve(this.pickUrl(), options.path); + const description = options.description || `${options.method} ${url}`; + const attempt = options.attempt === undefined ? 1 : options.attempt; + const maxAttempts = + options.maxAttempts === undefined ? DEFAULT_MAX_ATTEMPTS : options.maxAttempts; + + try { + const response = await Axios.request({ + method: options.method, + url, + data: options.body, + params: options.query, + headers: { + 'kbn-xsrf': 'kbn-client', + }, + }); + + return response.data; + } catch (error) { + let retryErrorMsg: string | undefined; + if (isAxiosRequestError(error)) { + retryErrorMsg = `[${description}] request failed (attempt=${attempt})`; + } else if (isConcliftOnGetError(error)) { + retryErrorMsg = `Conflict on GET (path=${options.path}, attempt=${attempt})`; + } + + if (retryErrorMsg) { + if (attempt < maxAttempts) { + this.log.error(retryErrorMsg); + await delay(1000 * attempt); + return await this.request({ + ...options, + attempt: attempt + 1, + }); + } + + throw new Error(retryErrorMsg + ' and ran out of retries'); + } + + throw error; + } + } +} diff --git a/test/common/services/kibana_server/saved_objects.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts similarity index 64% rename from test/common/services/kibana_server/saved_objects.ts rename to packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts index 0e4a9a34bf2e40..51fa19c140bf05 100644 --- a/test/common/services/kibana_server/saved_objects.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_saved_objects.ts @@ -17,16 +17,9 @@ * under the License. */ -import Url from 'url'; +import { ToolingLog } from '../tooling_log'; -import Axios, { AxiosRequestConfig } from 'axios'; -import { ToolingLog } from '@kbn/dev-utils'; - -const joinPath = (...components: Array) => - `/${components - .filter((s): s is string => !!s) - .map(c => encodeURIComponent(c)) - .join('/')}`; +import { KbnClientRequester, uriencode } from './kbn_client_requester'; type MigrationVersion = Record; @@ -64,15 +57,8 @@ interface UpdateOptions extends IndexOptions { id: string; } -export class KibanaServerSavedObjects { - private readonly x = Axios.create({ - baseURL: Url.resolve(this.url, '/api/saved_objects/'), - headers: { - 'kbn-xsrf': 'KibanaServerSavedObjects', - }, - }); - - constructor(private readonly url: string, private readonly log: ToolingLog) {} +export class KbnClientSavedObjects { + constructor(private readonly log: ToolingLog, private readonly requester: KbnClientRequester) {} /** * Get an object @@ -80,8 +66,9 @@ export class KibanaServerSavedObjects { public async get>(options: GetOptions) { this.log.debug('Gettings saved object: %j', options); - return await this.request>('get saved object', { - url: joinPath(options.type, options.id), + return await this.requester.request>({ + description: 'get saved object', + path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'GET', }); } @@ -92,13 +79,16 @@ export class KibanaServerSavedObjects { public async create>(options: IndexOptions) { this.log.debug('Creating saved object: %j', options); - return await this.request>('update saved object', { - url: joinPath(options.type, options.id), - params: { + return await this.requester.request>({ + description: 'update saved object', + path: options.id + ? uriencode`/api/saved_objects/${options.type}/${options.id}` + : uriencode`/api/saved_objects/${options.type}`, + query: { overwrite: options.overwrite, }, method: 'POST', - data: { + body: { attributes: options.attributes, migrationVersion: options.migrationVersion, references: options.references, @@ -112,13 +102,14 @@ export class KibanaServerSavedObjects { public async update>(options: UpdateOptions) { this.log.debug('Updating saved object: %j', options); - return await this.request>('update saved object', { - url: joinPath(options.type, options.id), - params: { + return await this.requester.request>({ + description: 'update saved object', + path: uriencode`/api/saved_objects/${options.type}/${options.id}`, + query: { overwrite: options.overwrite, }, method: 'PUT', - data: { + body: { attributes: options.attributes, migrationVersion: options.migrationVersion, references: options.references, @@ -132,22 +123,10 @@ export class KibanaServerSavedObjects { public async delete(options: GetOptions) { this.log.debug('Deleting saved object %s/%s', options); - return await this.request('delete saved object', { - url: joinPath(options.type, options.id), + return await this.requester.request({ + description: 'delete saved object', + path: uriencode`/api/saved_objects/${options.type}/${options.id}`, method: 'DELETE', }); } - - private async request(desc: string, options: AxiosRequestConfig) { - try { - const resp = await this.x.request(options); - return resp.data; - } catch (error) { - if (error.response) { - throw new Error(`Failed to ${desc}:\n${JSON.stringify(error.response.data, null, 2)}`); - } - - throw error; - } - } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts new file mode 100644 index 00000000000000..22baf4a3304168 --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_status.ts @@ -0,0 +1,68 @@ +/* + * 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 { KbnClientRequester } from './kbn_client_requester'; + +interface Status { + state: 'green' | 'red' | 'yellow'; + title?: string; + id?: string; + icon: string; + message: string; + uiColor: string; + since: string; +} + +interface ApiResponseStatus { + name: string; + uuid: string; + version: { + number: string; + build_hash: string; + build_number: number; + build_snapshot: boolean; + }; + status: { + overall: Status; + statuses: Status[]; + }; + metrics: unknown; +} + +export class KbnClientStatus { + constructor(private readonly requester: KbnClientRequester) {} + + /** + * Get the full server status + */ + async get() { + return await this.requester.request({ + method: 'GET', + path: 'api/status', + }); + } + + /** + * Get the overall/merged state + */ + public async getOverallState() { + const status = await this.get(); + return status.status.overall.state; + } +} diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts new file mode 100644 index 00000000000000..03033bc5c2ccc4 --- /dev/null +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -0,0 +1,113 @@ +/* + * 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 '../tooling_log'; + +import { KbnClientRequester, uriencode } from './kbn_client_requester'; + +export type UiSettingValues = Record; +interface UiSettingsApiResponse { + settings: { + [key: string]: { + userValue: string | number | boolean; + isOverridden: boolean | undefined; + }; + }; +} + +export class KbnClientUiSettings { + constructor( + private readonly log: ToolingLog, + private readonly requester: KbnClientRequester, + private readonly defaults?: UiSettingValues + ) {} + + async get(setting: string) { + const all = await this.getAll(); + const value = all.settings[setting] ? all.settings[setting].userValue : undefined; + + this.log.verbose('uiSettings.value: %j', value); + return value; + } + + /** + * Gets defaultIndex from the config doc. + */ + async getDefaultIndex() { + return await this.get('defaultIndex'); + } + + /** + * Unset a uiSetting + */ + async unset(setting: string) { + return await this.requester.request({ + path: uriencode`/api/kibana/settings/${setting}`, + method: 'DELETE', + }); + } + + /** + * Replace all uiSettings with the `doc` values, `doc` is merged + * with some defaults + */ + async replace(doc: UiSettingValues) { + const all = await this.getAll(); + for (const [name, { isOverridden }] of Object.entries(all.settings)) { + if (!isOverridden) { + await this.unset(name); + } + } + + this.log.debug('replacing kibana config doc: %j', doc); + + await this.requester.request({ + method: 'POST', + path: '/api/kibana/settings', + body: { + changes: { + ...this.defaults, + ...doc, + }, + }, + }); + } + + /** + * Add fields to the config doc (like setting timezone and defaultIndex) + */ + async update(updates: UiSettingValues) { + this.log.debug('applying update to kibana config: %j', updates); + + await this.requester.request({ + path: '/api/kibana/settings', + method: 'POST', + body: { + changes: updates, + }, + }); + } + + private async getAll() { + return await this.requester.request({ + path: '/api/kibana/settings', + method: 'GET', + }); + } +} diff --git a/src/legacy/ui/ui_exports/ui_exports_mixin.js b/packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts similarity index 63% rename from src/legacy/ui/ui_exports/ui_exports_mixin.js rename to packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts index ea2a07f3b265e0..1aacb857f12f67 100644 --- a/src/legacy/ui/ui_exports/ui_exports_mixin.js +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_version.ts @@ -17,22 +17,20 @@ * under the License. */ -import { collectUiExports } from './collect_ui_exports'; +import { KbnClientStatus } from './kbn_client_status'; -export function uiExportsMixin(kbnServer) { - kbnServer.uiExports = collectUiExports( - kbnServer.pluginSpecs - ); +export class KbnClientVersion { + private versionCache: string | undefined; - // check for unknown uiExport types - const { unknown = [] } = kbnServer.uiExports; - if (!unknown.length) { - return; - } + constructor(private readonly status: KbnClientStatus) {} + + async get() { + if (this.versionCache !== undefined) { + return this.versionCache; + } - throw new Error(`Unknown uiExport types: ${ - unknown - .map(({ pluginSpec, type }) => `${type} from ${pluginSpec.getId()}`) - .join(', ') - }`); + const status = await this.status.get(); + this.versionCache = status.version.number + (status.version.build_snapshot ? '-SNAPSHOT' : ''); + return this.versionCache; + } } diff --git a/packages/kbn-es-query/src/es_query/index.d.ts b/packages/kbn-es-query/src/es_query/index.d.ts new file mode 100644 index 00000000000000..9510a18441e53c --- /dev/null +++ b/packages/kbn-es-query/src/es_query/index.d.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function buildQueryFromFilters(filters: unknown[], indexPattern: unknown): unknown; +export function buildEsQuery( + indexPattern: unknown, + queries: unknown, + filters: unknown, + config?: { + allowLeadingWildcards: boolean; + queryStringOptions: unknown; + ignoreFilterIfFieldNotInIndex: boolean; + dateFormatTZ?: string | null; + } +): unknown; +export function getEsQueryConfig(config: { + get: (name: string) => unknown; +}): { + allowLeadingWildcards: boolean; + queryStringOptions: unknown; + ignoreFilterIfFieldNotInIndex: boolean; + dateFormatTZ?: string | null; +}; diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts index 873636a28889fd..ca4455da33f45d 100644 --- a/packages/kbn-es-query/src/index.d.ts +++ b/packages/kbn-es-query/src/index.d.ts @@ -17,5 +17,6 @@ * under the License. */ +export * from './es_query'; export * from './kuery'; export * from './filters'; diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts index 915c024f2ab48d..448ef0e9cca750 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.d.ts @@ -17,6 +17,8 @@ * under the License. */ +import { JsonObject } from '..'; + /** * WARNING: these typings are incomplete */ @@ -30,15 +32,6 @@ export interface KueryParseOptions { startRule: string; } -type JsonValue = null | boolean | number | string | JsonObject | JsonArray; - -interface JsonObject { - [key: string]: JsonValue; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface JsonArray extends Array {} - export function fromKueryExpression( expression: string, parseOptions?: KueryParseOptions diff --git a/packages/kbn-es-query/src/kuery/functions/is.js b/packages/kbn-es-query/src/kuery/functions/is.js index 0338671e9b3fe4..690f98b08ba827 100644 --- a/packages/kbn-es-query/src/kuery/functions/is.js +++ b/packages/kbn-es-query/src/kuery/functions/is.js @@ -32,7 +32,6 @@ export function buildNodeParams(fieldName, value, isPhrase = false) { if (_.isUndefined(value)) { throw new Error('value is a required argument'); } - const fieldNode = typeof fieldName === 'string' ? ast.fromLiteralExpression(fieldName) : literal.buildNode(fieldName); const valueNode = typeof value === 'string' ? ast.fromLiteralExpression(value) : literal.buildNode(value); const isPhraseNode = literal.buildNode(isPhrase); @@ -42,7 +41,7 @@ export function buildNodeParams(fieldName, value, isPhrase = false) { } export function toElasticsearchQuery(node, indexPattern = null, config = {}) { - const { arguments: [ fieldNameArg, valueArg, isPhraseArg ] } = node; + const { arguments: [fieldNameArg, valueArg, isPhraseArg] } = node; const fieldName = ast.toElasticsearchQuery(fieldNameArg); const value = !_.isUndefined(valueArg) ? ast.toElasticsearchQuery(valueArg) : valueArg; const type = isPhraseArg.value ? 'phrase' : 'best_fields'; diff --git a/packages/kbn-es-query/src/kuery/index.d.ts b/packages/kbn-es-query/src/kuery/index.d.ts index 9d797406420d41..b01a8914f68ef3 100644 --- a/packages/kbn-es-query/src/kuery/index.d.ts +++ b/packages/kbn-es-query/src/kuery/index.d.ts @@ -18,3 +18,13 @@ */ export * from './ast'; +export { nodeTypes } from './node_types'; + +export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +export interface JsonObject { + [key: string]: JsonValue; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface JsonArray extends Array {} diff --git a/packages/kbn-es-query/src/kuery/index.js b/packages/kbn-es-query/src/kuery/index.js index 84c6a205b42ce6..08fa9829d4a566 100644 --- a/packages/kbn-es-query/src/kuery/index.js +++ b/packages/kbn-es-query/src/kuery/index.js @@ -19,5 +19,5 @@ export * from './ast'; export * from './filter_migration'; -export * from './node_types'; +export { nodeTypes } from './node_types'; export * from './errors'; diff --git a/packages/kbn-es-query/src/kuery/node_types/index.d.ts b/packages/kbn-es-query/src/kuery/node_types/index.d.ts new file mode 100644 index 00000000000000..0d1f2c28e39f08 --- /dev/null +++ b/packages/kbn-es-query/src/kuery/node_types/index.d.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * WARNING: these typings are incomplete + */ + +import { JsonObject, JsonValue } from '..'; + +type FunctionName = + | 'is' + | 'and' + | 'or' + | 'not' + | 'range' + | 'exists' + | 'geoBoundingBox' + | 'geoPolygon'; + +interface FunctionTypeBuildNode { + type: 'function'; + function: FunctionName; + // TODO -> Need to define a better type for DSL query + arguments: any[]; +} + +interface FunctionType { + buildNode: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + buildNodeWithArgumentNodes: (functionName: FunctionName, ...args: any[]) => FunctionTypeBuildNode; + toElasticsearchQuery: (node: any, indexPattern: any, config: JsonObject) => JsonValue; +} + +interface LiteralType { + buildNode: ( + value: null | boolean | number | string + ) => { type: 'literal'; value: null | boolean | number | string }; + toElasticsearchQuery: (node: any) => null | boolean | number | string; +} + +interface NamedArgType { + buildNode: (name: string, value: any) => { type: 'namedArg'; name: string; value: any }; + toElasticsearchQuery: (node: any) => string; +} + +interface WildcardType { + buildNode: (value: string) => { type: 'wildcard'; value: string }; + test: (node: any, string: string) => boolean; + toElasticsearchQuery: (node: any) => string; + toQueryStringQuery: (node: any) => string; + hasLeadingWildcard: (node: any) => boolean; +} + +interface NodeTypes { + function: FunctionType; + literal: LiteralType; + namedArg: NamedArgType; + wildcard: WildcardType; +} + +export const nodeTypes: NodeTypes; diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index 18e161e0ce925e..523317ab63e85b 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -88,7 +88,12 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug await withProcRunner(log, async proc => { await proc.run('kibana', { cmd: 'yarn', - args: ['start', '--optimize.enabled=false', '--logging.json=false'], + args: [ + 'start', + '--optimize.enabled=false', + '--logging.json=false', + '--migrations.skip=true', + ], cwd: generatedPath, wait: /ispec_plugin.+Status changed from uninitialized to green - Ready/, }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js index fb1ca192c5fd37..9901f62ae71cf3 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js @@ -95,16 +95,16 @@ it('only runs hooks of parents and tests in level1a', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1a test 1a", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1a", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1a test 1a", + ] + `); }); it('only runs hooks of parents and tests in level1b', async () => { @@ -114,16 +114,16 @@ it('only runs hooks of parents and tests in level1b', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1b' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1b test 1b", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1b' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1b", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1b test 1b", + ] + `); }); it('only runs hooks of parents and tests in level1a and level1b', async () => { @@ -133,20 +133,20 @@ it('only runs hooks of parents and tests in level1a and level1b', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a', 'level1b' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1a test 1a", - "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1b test 1b", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1a', 'level1b' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1a", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1a test 1a", + "suite: level 1 level 1b", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1b test 1b", + ] + `); }); it('only runs level1a if including level1 and excluding level1b', async () => { @@ -156,17 +156,17 @@ it('only runs level1a if including level1 and excluding level1b', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", - "info: Filtering out any suites that include the tag(s): [ 'level1b' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1a", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1a test 1a", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", + "info: Filtering out any suites that include the tag(s): [ 'level1b' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1a", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1a test 1a", + ] + `); }); it('only runs level1b if including level1 and excluding level1a', async () => { @@ -176,17 +176,17 @@ it('only runs level1b if including level1 and excluding level1a', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", - "info: Filtering out any suites that include the tag(s): [ 'level1a' ]", - "suite: ", - "suite: level 1", - "suite: level 1 level 1b", - "hook: \\"before each\\" hook: rootBeforeEach", - "hook: level 1 \\"before each\\" hook: level1BeforeEach", - "test: level 1 level 1b test 1b", -] -`); + Array [ + "info: Only running suites (and their sub-suites) if they include the tag(s): [ 'level1' ]", + "info: Filtering out any suites that include the tag(s): [ 'level1a' ]", + "suite: ", + "suite: level 1", + "suite: level 1 level 1b", + "hook: \\"before each\\" hook: rootBeforeEach", + "hook: level 1 \\"before each\\" hook: level1BeforeEach", + "test: level 1 level 1b test 1b", + ] + `); }); it('only runs level2 if excluding level1', async () => { @@ -196,15 +196,15 @@ it('only runs level2 if excluding level1', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Filtering out any suites that include the tag(s): [ 'level1' ]", - "suite: ", - "suite: level 2", - "suite: level 2 level 2a", - "hook: \\"before each\\" hook: rootBeforeEach", - "test: level 2 level 2a test 2a", -] -`); + Array [ + "info: Filtering out any suites that include the tag(s): [ 'level1' ]", + "suite: ", + "suite: level 2", + "suite: level 2 level 2a", + "hook: \\"before each\\" hook: rootBeforeEach", + "test: level 2 level 2a test 2a", + ] + `); }); it('does nothing if everything excluded', async () => { @@ -214,8 +214,8 @@ it('does nothing if everything excluded', async () => { }); expect(history).toMatchInlineSnapshot(` -Array [ - "info: Filtering out any suites that include the tag(s): [ 'level1', 'level2a' ]", -] -`); + Array [ + "info: Filtering out any suites that include the tag(s): [ 'level1', 'level2a' ]", + ] + `); }); diff --git a/renovate.json5 b/renovate.json5 index e5dd29aa631908..cd836762b817f8 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -67,6 +67,17 @@ '(\\b|_)jest(\\b|_)', ], }, + { + groupSlug: '@elastic/charts', + groupName: '@elastic/charts related packages', + packageNames: [ + '@elastic/charts', + '@types/elastic__charts', + ], + reviewers: [ + 'markov00', + ], + }, { groupSlug: 'mocha', groupName: 'mocha related packages', diff --git a/rfcs/text/0006_management_section_service.md b/rfcs/text/0006_management_section_service.md new file mode 100644 index 00000000000000..bcb74b1bcd8da7 --- /dev/null +++ b/rfcs/text/0006_management_section_service.md @@ -0,0 +1,332 @@ +- Start Date: 2019-08-20 +- RFC PR: TBD +- Kibana Issue: [#43499](https://github.com/elastic/kibana/issues/43499) + +# Summary +Management is one of the four primary "domains" covered by @elastic/kibana-app-arch (along with Data, Embeddables, and Visualizations). There are two main purposes for this service: + +1. Own the management "framework" -- the UI that displays the management sidebar nav, the landing page, and handles rendering each of the sections +2. Expose a registry for other plugins to add their own registry sections to the UI and add nested links to them in the sidebar. + +The purpose of this RFC is to consider item 2 above -- the service for registering sections to the nav & loading them up. + +# Motivation + +## Why now? +The main driver for considering this now is that the Management API moving to the new platform is going to block other teams from completing migration, so we need to have an answer to what the new platform version of the API looks like as soon as possible in `7.x`. + +## Why not just keep the current API and redesign later? +The answer to that has to do with the items that are currently used in the management implementation which must be removed in order to migrate to NP: the framework currently registers a `uiExport`, and relies on `IndexedArray`, `uiRegistry`, and `ui/routes`. + +This means that we will basically need to rebuild the service anyway in order to migrate to the new platform. So if we are going to invest that time, we might as well invest it in building the API the way we want it to be longer term, rather than creating more work for ourselves later. + +## Technical goals +- Remove another usage of `IndexedArray` & `uiRegistry` (required for migration) +- Remove dependency on `ui/routes` (required for migration) +- Remove management section `uiExport` (required for migration) +- Simple API that is designed in keeping with new platform principles + - This includes being rendering-framework-agnostic... You should be able to build your management section UI however you'd like +- Clear separation of app/UI code and service code, even if both live within the same plugin +- Flexibility to potentially support alternate layouts in the future (see mockups in [reference section](#reference) below) + +# Basic example +This API is influenced heavily by the [application service mounting RFC](https://github.com/elastic/kibana/blob/master/rfcs/text/0004_application_service_mounting.md). The intent is to make the experience consistent with that service; the Management section is basically one big app with a bunch of registered "subapps". + +```ts +// my_plugin/public/plugin.ts + +export class MyPlugin { + setup(core, { management }) { + // Registering a new app to a new section + const mySection = management.sections.register({ + id: 'my-section', + title: 'My Main Section', // display name + order: 10, + euiIconType: 'iconName', + }); + mySection.registerApp({ + id: 'my-management-app', + title: 'My Management App', // display name + order: 20, + async mount(context, params) { + const { renderApp } = await import('./my-section'); + return renderApp(context, params); + } + }); + + // Registering a new app to an existing section + const kibanaSection = management.sections.get('kibana'); + kibanaSection.registerApp({ id: 'my-kibana-management-app', ... }); + } + + start(core, { management }) { + // access all registered sections, filtered based on capabilities + const sections = management.sections.getAvailable(); + sections.forEach(section => console.log(`${section.id} - ${section.title}`)); + // automatically navigate to any app by id + management.sections.navigateToApp('my-kibana-management-app'); + } +} + +// my_plugin/public/my-section.tsx + +export function renderApp(context, { sectionBasePath, element }) { + ReactDOM.render( + // `sectionBasePath` would be `/app/management/my-section/my-management-app` + , + element + ); + + // return value must be a function that unmounts (just like Core Application Service) + return () => ReactDOM.unmountComponentAtNode(element); +} +``` + +We can also create a utility in `kibana_react` to make it easy for folks to `mount` a React app: +```ts +// src/plugins/kibana_react/public/mount_with_react.tsx +import { KibanaContextProvider } from './context'; + +export const mountWithReact = ( + Component: React.ComponentType<{ basename: string }>, + context: AppMountContext, + params: ManagementSectionMountParams, +) => { + ReactDOM.render( + ( + + + + ), + params.element + ); + + return () => ReactDOM.unmountComponentAtNode(params.element); +} + +// my_plugin/public/plugin.ts +import { mountWithReact } from 'src/plugins/kibana_react/public'; + +export class MyPlugin { + setup(core, { management }) { + const kibanaSection = management.sections.get('kibana'); + kibanaSection.registerApp({ + id: 'my-other-kibana-management-app', + ..., + async mount(context, params) { + const { MySection } = await import('./components/my-section'); + const unmountCallback = mountWithReact(MySection, context, params); + return () => unmountCallback(); + } + }); + } +} +``` + +# Detailed design + +```ts +interface ManagementSetup { + sections: SectionsServiceSetup; +} + +interface ManagementStart { + sections: SectionsServiceStart; +} + +interface SectionsServiceSetup { + get: (sectionId: string) => Section; + getAvailable: () => Section[]; // filtered based on capabilities + register: RegisterSection; +} + +interface SectionsServiceStart { + getAvailable: () => Array>; // filtered based on capabilities + // uses `core.application.navigateToApp` under the hood, automatically prepending the `path` for the link + navigateToApp: (appId: string, options?: { path?: string; state?: any }) => void; +} + +type RegisterSection = ( + id: string, + title: string, + order?: number, + euiIconType?: string, // takes precedence over `icon` property. + icon?: string, // URL to image file; fallback if no `euiIconType` +) => Section; + +type RegisterManagementApp = ( + id: string; + title: string; + order?: number; + mount: ManagementSectionMount; +) => ManagementApp; + +type Unmount = () => Promise | void; + +interface ManagementSectionMountParams { + sectionBasePath: string; // base path for setting up your router + element: HTMLElement; // element the section should render into +} + +type ManagementSectionMount = ( + context: AppMountContext, // provided by core.ApplicationService + params: ManagementSectionMountParams, +) => Unmount | Promise; + +interface ManagementApp { + id: string; + title: string; + basePath: string; + sectionId: string; + order?: number; +} + +interface Section { + id: string; + title: string; + apps: ManagementApp[]; + registerApp: RegisterManagementApp; + order?: number; + euiIconType?: string; + icon?: string; +} +``` + +# Legacy service (what this would be replacing) + +Example of how this looks today: +```js +// myplugin/index +new Kibana.Plugin({ + uiExports: { + managementSections: ['myplugin/management'], + } +}); + +// myplugin/public/management +import { management } from 'ui/management'; + +// completely new section +const newSection = management.register('mypluginsection', { + name: 'mypluginsection', + order: 10, + display: 'My Plugin', + icon: 'iconName', +}); +newSection.register('mypluginlink', { + name: 'mypluginlink', + order: 10, + display: 'My sublink', + url: `#/management/myplugin`, +}); + +// new link in existing section +const kibanaSection = management.getSection('kibana'); +kibanaSection.register('mypluginlink', { + name: 'mypluginlink', + order: 10, + display: 'My sublink', + url: `#/management/myplugin`, +}); + +// use ui/routes to render component +import routes from 'ui/routes'; + +const renderReact = (elem) => { + render(, elem); +}; + +routes.when('management/myplugin', { + controller($scope, $http, kbnUrl) { + $scope.$on('$destroy', () => { + const elem = document.getElementById('usersReactRoot'); + if (elem) unmountComponentAtNode(elem); + }); + $scope.$$postDigest(() => { + const elem = document.getElementById('usersReactRoot'); + const changeUrl = (url) => { + kbnUrl.change(url); + $scope.$apply(); + }; + renderReact(elem, $http, changeUrl); + }); + }, +}); +``` +Current public contracts owned by the legacy service: +```js +// ui/management/index +interface API { + PAGE_TITLE_COMPONENT: string; // actually related to advanced settings? + PAGE_SUBTITLE_COMPONENT: string; // actually related to advanced settings? + PAGE_FOOTER_COMPONENT: string; // actually related to advanced settings? + SidebarNav: React.SFC; + registerSettingsComponent: ( + id: string, + component: string | React.SFC, + allowOverride: boolean + ) => void; + management: new ManagementSection(); + MANAGEMENT_BREADCRUMB: { + text: string; + href: string; + }; +} + +// ui/management/section +class ManagementSection { + get visibleItems, + addListener: (fn: function) => void, + register: (id: string, options: Options) => ManagementSection, + deregister: (id: string) => void, + hasItem: (id: string) => boolean, + getSection: (id: string) => ManagementSection, + hide: () => void, + show: () => void, + disable: () => void, + enable: () => void, +} + +interface Options { + order: number | null; + display: string | null; // defaults to id + url: string | null; // defaults to '' + visible: boolean | null; // defaults to true + disabled: boolean | null; // defaults to false + tooltip: string | null; // defaults to '' + icon: string | null; // defaults to '' +} +``` + +# Notes + +- The hide/show/disable/enable options were dropped with the assumption that we will be working with uiCapabilities to determine this instead... so people shouldn't need to manage it manually as they can look up a pre-filtered list of sections. +- This was updated to add flexibility for custom (non-EUI) icons as outlined in [#32661](https://github.com/elastic/kibana/issues/32661). Much like the Core Application Service, you either choose an EUI icon, or provide a URL to an icon. + +# Drawbacks + +- This removes the ability to infinitely nest sections within each other by making a distinction between a section header and a nav link. + - So far we didn't seem to be using this feature anyway, but would like feedback on any use cases for it. + +# Reference + +- Issues about Global vs Spaces-based management sections: https://github.com/elastic/kibana/issues/37285 https://github.com/elastic/kibana/issues/37283 +- Mockups related to above issues: https://marvelapp.com/52b8616/screen/57582729 + +# Alternatives + +An alternative design would be making everything React-specific and simply requiring consumers of the service to provide a React component to render when a route is hit, or giving them a react-router instance to work with. + +This would require slightly less work for folks using the service as it would eliminate the need for a `mount` function. However, it comes at the cost of forcing folks into a specific rendering framework, which ultimately provides less flexibility. + +# Adoption strategy + +Our strategy for implementing this should be to build the service entirely in the new platform in a `management` plugin, so that plugins can gradually cut over to the new service as they prepare to migrate to the new platform. + +One thing we would need to figure out is how to bridge the gap between the new plugin and the legacy `ui/management` service. Ideally we would find a way to integrate the two, such that the management nav could display items registered via both services. This is a strategy we'd need to work out in more detail as we got closer to implementation. + +# How we teach this + +The hope is that this will already feel familiar to Kibana application developers, as most will have already been exposed to the Core Application Service and how it handles mounting. + +A guide could also be added to the "Management" section of the Kibana docs (the legacy service is not even formally documented). diff --git a/src/cli/serve/integration_tests/invalid_config.test.js b/src/cli/serve/integration_tests/invalid_config.test.js index a3f44697281090..e86fb03ad79546 100644 --- a/src/cli/serve/integration_tests/invalid_config.test.js +++ b/src/cli/serve/integration_tests/invalid_config.test.js @@ -25,9 +25,12 @@ const INVALID_CONFIG_PATH = resolve(__dirname, '__fixtures__/invalid_config.yml' describe('cli invalid config support', function () { it('exits with statusCode 64 and logs a single line when config is invalid', function () { + // Unused keys only throw once LegacyService starts, so disable migrations so that Core + // will finish the start lifecycle without a running Elasticsearch instance. const { error, status, stdout } = spawnSync(process.execPath, [ 'src/cli', - '--config', INVALID_CONFIG_PATH + '--config', INVALID_CONFIG_PATH, + '--migrations.skip=true' ], { cwd: ROOT_DIR }); 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 2b6f229ca9dae4..206118d2d1be85 100644 --- a/src/cli/serve/integration_tests/reload_logging_config.test.js +++ b/src/cli/serve/integration_tests/reload_logging_config.test.js @@ -83,7 +83,7 @@ describe('Server logging configuration', function () { it('should be reloadable via SIGHUP process signaling', async function () { expect.assertions(3); - child = spawn(process.execPath, [kibanaPath, '--config', testConfigFile, '--oss'], { + child = spawn(process.execPath, [kibanaPath, '--config', testConfigFile, '--oss', '--verbose'], { stdio: 'pipe' }); @@ -114,7 +114,9 @@ describe('Server logging configuration', function () { const data = JSON.parse(line); sawJson = true; - if (data.tags.includes('listening')) { + // We know the sighup handler will be registered before + // root.setup() is called + if (data.message.includes('setting up root')) { isJson = false; setLoggingJson(false); @@ -128,10 +130,9 @@ describe('Server logging configuration', function () { // the switch yet, so we ignore before switching over. } else { // Kibana has successfully stopped logging json, so kill the server. - sawNonjson = true; - child.kill(); + child && child.kill(); child = undefined; } }) @@ -178,10 +179,11 @@ describe('Server logging configuration', function () { '--config', testConfigFile, '--logging.dest', logPath, '--plugins.initialize', 'false', - '--logging.json', 'false' + '--logging.json', 'false', + '--verbose' ]); - watchFileUntil(logPath, /http server running/, 2 * minute) + watchFileUntil(logPath, /starting server/, 2 * minute) .then(() => { // once the server is running, archive the log file and issue SIGHUP fs.renameSync(logPath, logPathArchived); @@ -190,8 +192,8 @@ describe('Server logging configuration', function () { .then(() => watchFileUntil(logPath, /Reloaded logging configuration due to SIGHUP/, 10 * second)) .then(contents => { const lines = contents.toString().split('\n'); - // should be the first and only new line of the log file - expect(lines).toHaveLength(2); + // should be the first line of the new log file + expect(lines[0]).toMatch(/Reloaded logging configuration due to SIGHUP/); child.kill(); }) .then(done, done); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 7f479a7e118e02..1f7593d788304b 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -194,7 +194,6 @@ export default function (program) { .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector) .option('--optimize', 'Optimize and then stop the server'); - if (CAN_REPL) { command.option('--repl', 'Run the server with a REPL prompt and access to the server object'); } @@ -240,7 +239,7 @@ export default function (program) { repl: !!opts.repl, basePath: !!opts.basePath, optimize: !!opts.optimize, - oss: !!opts.oss, + oss: !!opts.oss }, features: { isClusterModeSupported: CAN_CLUSTER, diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index d1855a0370f002..935844baddf86a 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -27,12 +27,9 @@ import { AppRouter } from './ui'; import { HttpStart } from '../http'; import { ContextSetup, IContextContainer } from '../context'; import { - AppMountContext, App, LegacyApp, AppMounter, - AppUnmount, - AppMountParameters, InternalApplicationSetup, InternalApplicationStart, } from './types'; @@ -64,11 +61,7 @@ export class ApplicationService { private readonly apps$ = new BehaviorSubject>(new Map()); private readonly legacyApps$ = new BehaviorSubject>(new Map()); private readonly capabilities = new CapabilitiesService(); - private mountContext?: IContextContainer< - AppMountContext, - AppUnmount | Promise, - [AppMountParameters] - >; + private mountContext?: IContextContainer; public setup({ context }: SetupDeps): InternalApplicationSetup { this.mountContext = context.createContextContainer(); @@ -98,7 +91,7 @@ export class ApplicationService { this.legacyApps$.next(new Map([...this.legacyApps$.value.entries(), [app.id, app]])); }, - registerMountContext: this.mountContext.registerContext, + registerMountContext: this.mountContext!.registerContext, }; } diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 018d7569ce411b..b2d0aff26b8b09 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -193,7 +193,7 @@ export interface ApplicationSetup { */ registerMountContext( contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -224,7 +224,7 @@ export interface InternalApplicationSetup { registerMountContext( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -261,7 +261,7 @@ export interface ApplicationStart { */ registerMountContext( contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -291,7 +291,7 @@ export interface InternalApplicationStart registerMountContext( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; // Internal APIs diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index afd9f8e4a38209..f24b0ed1681aab 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -19,7 +19,7 @@ import Url from 'url'; -import React, { Component, createRef, Fragment } from 'react'; +import React, { Component, createRef } from 'react'; import * as Rx from 'rxjs'; import { @@ -376,7 +376,7 @@ class HeaderUI extends Component { ]; return ( - +
@@ -407,11 +407,13 @@ class HeaderUI extends Component { isLocked={isLocked} onIsLockedUpdate={onIsLockedUpdate} > - - - + - +
); } diff --git a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx index f4b1c1d49cd27f..68eb6a54f48a3c 100644 --- a/src/core/public/chrome/ui/header/header_breadcrumbs.tsx +++ b/src/core/public/chrome/ui/header/header_breadcrumbs.tsx @@ -64,7 +64,11 @@ export class HeaderBreadcrumbs extends Component { public render() { return ( - + ); } diff --git a/src/core/public/context/context_service.ts b/src/core/public/context/context_service.ts index 704524d8386367..dadc509c97821e 100644 --- a/src/core/public/context/context_service.ts +++ b/src/core/public/context/context_service.ts @@ -18,7 +18,7 @@ */ import { PluginOpaqueId } from '../../server'; -import { IContextContainer, ContextContainer } from '../../utils/context'; +import { IContextContainer, ContextContainer, HandlerFunction } from '../../utils/context'; import { CoreContext } from '../core_system'; interface StartDeps { @@ -31,15 +31,8 @@ export class ContextService { public setup({ pluginDependencies }: StartDeps): ContextSetup { return { - createContextContainer: < - TContext extends {}, - THandlerReturn, - THandlerParameters extends any[] = [] - >() => - new ContextContainer( - pluginDependencies, - this.core.coreId - ), + createContextContainer: >() => + new ContextContainer(pluginDependencies, this.core.coreId), }; } } @@ -111,9 +104,5 @@ export interface ContextSetup { /** * Creates a new {@link IContextContainer} for a service owner. */ - createContextContainer< - TContext extends {}, - THandlerReturn, - THandlerParmaters extends any[] = [] - >(): IContextContainer; + createContextContainer>(): IContextContainer; } diff --git a/src/core/public/context/index.ts b/src/core/public/context/index.ts index 28b2641b2a5a74..f22c4168d75448 100644 --- a/src/core/public/context/index.ts +++ b/src/core/public/context/index.ts @@ -18,4 +18,10 @@ */ export { ContextService, ContextSetup } from './context_service'; -export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context'; +export { + IContextContainer, + IContextProvider, + HandlerFunction, + HandlerContextType, + HandlerParameters, +} from '../../utils/context'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 393a7076759e86..1e2dfde7496ea2 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -67,7 +67,14 @@ import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './u import { ApplicationSetup, Capabilities, ApplicationStart } from './application'; import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; -import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } from './context'; +import { + IContextContainer, + IContextProvider, + ContextSetup, + HandlerFunction, + HandlerContextType, + HandlerParameters, +} from './context'; export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; @@ -217,7 +224,9 @@ export { ChromeRecentlyAccessedHistoryItem, ChromeStart, IContextContainer, - IContextHandler, + HandlerFunction, + HandlerContextType, + HandlerParameters, IContextProvider, ContextSetup, DocLinksStart, diff --git a/src/core/public/notifications/notifications_service.ts b/src/core/public/notifications/notifications_service.ts index 2dc2b2ef06094f..33221522fa83ca 100644 --- a/src/core/public/notifications/notifications_service.ts +++ b/src/core/public/notifications/notifications_service.ts @@ -48,7 +48,7 @@ export class NotificationsService { public setup({ uiSettings }: SetupDeps): NotificationsSetup { const notificationSetup = { toasts: this.toasts.setup({ uiSettings }) }; - this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe(error => { + this.uiSettingsErrorSubscription = uiSettings.getUpdateErrors$().subscribe((error: Error) => { notificationSetup.toasts.addDanger({ title: i18n.translate('core.notifications.unableUpdateUISettingNotificationMessageTitle', { defaultMessage: 'Unable to update UI setting', diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b2d730d7fa4670..e6c8f116e5782a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -9,6 +9,7 @@ import { MouseEventHandler } from 'react'; import { Observable } from 'rxjs'; import React from 'react'; import * as Rx from 'rxjs'; +import { ShallowPromise } from '@kbn/utility-types'; import { EuiGlobalToastListToast as Toast } from '@elastic/eui'; // @public @@ -31,7 +32,7 @@ export interface AppBase { // @public (undocumented) export interface ApplicationSetup { register(app: App): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public (undocumented) @@ -44,7 +45,7 @@ export interface ApplicationStart { path?: string; state?: any; }): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public @@ -213,7 +214,7 @@ export interface ChromeStart { // @public export interface ContextSetup { - createContextContainer(): IContextContainer; + createContextContainer>(): IContextContainer; } // @internal (undocumented) @@ -389,6 +390,15 @@ export interface FatalErrorsSetup { get$: () => Rx.Observable; } +// @public +export type HandlerContextType> = T extends HandlerFunction ? U : never; + +// @public +export type HandlerFunction = (context: T, ...args: any[]) => any; + +// @public +export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; + // @public (undocumented) export type HttpBody = BodyInit | null | any; @@ -551,16 +561,13 @@ export interface I18nStart { } // @public -export interface IContextContainer { - createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise; - registerContext(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; +export interface IContextContainer> { + createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; + registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; } // @public -export type IContextHandler = (context: TContext, ...rest: THandlerParameters) => TReturn; - -// @public -export type IContextProvider, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; +export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: Partial>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; // @public @deprecated export interface LegacyCoreSetup extends CoreSetup { @@ -752,7 +759,7 @@ export class SavedObjectsClient { }[]) => Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } @@ -775,6 +782,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // (undocumented) + filter?: string; + // (undocumented) hasReference?: { type: string; id: string; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index dc13d001643a31..cf0300157aece3 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -297,6 +297,7 @@ export class SavedObjectsClient { searchFields: 'search_fields', sortField: 'sort_field', type: 'type', + filter: 'filter', }; const renamedQuery = renameKeys(renameMap, options); diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 6a4bfc7c581df8..2dff4430b4dbef 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -70,6 +70,22 @@ export async function bootstrap({ const root = new Root(rawConfigService.getConfig$(), env, onRootShutdown); + process.on('SIGHUP', () => { + const cliLogger = root.logger.get('cli'); + cliLogger.info('Reloading logging configuration due to SIGHUP.', { tags: ['config'] }); + + try { + rawConfigService.reloadConfig(); + } catch (err) { + return shutdown(err); + } + + cliLogger.info('Reloaded logging configuration due to SIGHUP.', { tags: ['config'] }); + }); + + process.on('SIGINT', () => shutdown()); + process.on('SIGTERM', () => shutdown()); + function shutdown(reason?: Error) { rawConfigService.stop(); return root.shutdown(reason); @@ -87,22 +103,6 @@ export async function bootstrap({ cliLogger.info('Optimization done.'); await shutdown(); } - - process.on('SIGHUP', () => { - const cliLogger = root.logger.get('cli'); - cliLogger.info('Reloading logging configuration due to SIGHUP.', { tags: ['config'] }); - - try { - rawConfigService.reloadConfig(); - } catch (err) { - return shutdown(err); - } - - cliLogger.info('Reloaded logging configuration due to SIGHUP.', { tags: ['config'] }); - }); - - process.on('SIGINT', () => shutdown()); - process.on('SIGTERM', () => shutdown()); } function onRootShutdown(reason?: any) { diff --git a/src/core/server/config/config_service.mock.ts b/src/core/server/config/config_service.mock.ts index b9c4fa91ae7028..e87869e92deebc 100644 --- a/src/core/server/config/config_service.mock.ts +++ b/src/core/server/config/config_service.mock.ts @@ -20,11 +20,13 @@ import { BehaviorSubject } from 'rxjs'; import { ObjectToConfigAdapter } from './object_to_config_adapter'; -import { ConfigService } from './config_service'; +import { IConfigService } from './config_service'; -type ConfigServiceContract = PublicMethodsOf; -const createConfigServiceMock = () => { - const mocked: jest.Mocked = { +const createConfigServiceMock = ({ + atPath = {}, + getConfig$ = {}, +}: { atPath?: Record; getConfig$?: Record } = {}) => { + const mocked: jest.Mocked = { atPath: jest.fn(), getConfig$: jest.fn(), optionalAtPath: jest.fn(), @@ -33,8 +35,8 @@ const createConfigServiceMock = () => { isEnabledAtPath: jest.fn(), setSchema: jest.fn(), }; - mocked.atPath.mockReturnValue(new BehaviorSubject({})); - mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter({}))); + mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); + mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); mocked.getUsedPaths.mockResolvedValue([]); mocked.getUnusedPaths.mockResolvedValue([]); mocked.isEnabledAtPath.mockResolvedValue(true); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index fff19aa3af0f04..8d3cc733cf250c 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -26,6 +26,9 @@ import { Config, ConfigPath, Env } from '.'; import { Logger, LoggerFactory } from '../logging'; import { hasConfigPathIntersection } from './config'; +/** @internal */ +export type IConfigService = PublicMethodsOf; + /** @internal */ export class ConfigService { private readonly log: Logger; diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 257263069cabd8..d27462a86a9c8e 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { ConfigService } from './config_service'; +export { ConfigService, IConfigService } from './config_service'; export { RawConfigService } from './raw_config_service'; export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config'; export { ObjectToConfigAdapter } from './object_to_config_adapter'; diff --git a/src/core/server/context/context_service.ts b/src/core/server/context/context_service.ts index 80935840c55360..1c5bd41a01f0f1 100644 --- a/src/core/server/context/context_service.ts +++ b/src/core/server/context/context_service.ts @@ -18,7 +18,7 @@ */ import { PluginOpaqueId } from '../../server'; -import { IContextContainer, ContextContainer } from '../../utils/context'; +import { IContextContainer, ContextContainer, HandlerFunction } from '../../utils/context'; import { CoreContext } from '../core_context'; interface SetupDeps { @@ -31,15 +31,8 @@ export class ContextService { public setup({ pluginDependencies }: SetupDeps): ContextSetup { return { - createContextContainer: < - TContext extends {}, - THandlerReturn, - THandlerParameters extends any[] = [] - >() => { - return new ContextContainer( - pluginDependencies, - this.core.coreId - ); + createContextContainer: >() => { + return new ContextContainer(pluginDependencies, this.core.coreId); }, }; } @@ -112,9 +105,5 @@ export interface ContextSetup { /** * Creates a new {@link IContextContainer} for a service owner. */ - createContextContainer< - TContext extends {}, - THandlerReturn, - THandlerParmaters extends any[] = [] - >(): IContextContainer; + createContextContainer>(): IContextContainer; } diff --git a/src/core/server/context/index.ts b/src/core/server/context/index.ts index 28b2641b2a5a74..f22c4168d75448 100644 --- a/src/core/server/context/index.ts +++ b/src/core/server/context/index.ts @@ -18,4 +18,10 @@ */ export { ContextService, ContextSetup } from './context_service'; -export { IContextContainer, IContextProvider, IContextHandler } from '../../utils/context'; +export { + IContextContainer, + IContextProvider, + HandlerFunction, + HandlerContextType, + HandlerParameters, +} from '../../utils/context'; diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts new file mode 100644 index 00000000000000..e8c0a0a4830bfb --- /dev/null +++ b/src/core/server/core_context.mock.ts @@ -0,0 +1,41 @@ +/* + * 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 { CoreContext } from './core_context'; +import { getEnvOptions } from './config/__mocks__/env'; +import { Env, IConfigService } from './config'; +import { loggingServiceMock } from './logging/logging_service.mock'; +import { configServiceMock } from './config/config_service.mock'; +import { ILoggingService } from './logging'; + +function create({ + env = Env.createDefault(getEnvOptions()), + logger = loggingServiceMock.create(), + configService = configServiceMock.create(), +}: { + env?: Env; + logger?: jest.Mocked; + configService?: jest.Mocked; +} = {}): CoreContext { + return { coreId: Symbol(), env, logger, configService }; +} + +export const mockCoreContext = { + create, +}; diff --git a/src/core/server/core_context.ts b/src/core/server/core_context.ts index 701f5a83a81c20..237fc2e6aafdce 100644 --- a/src/core/server/core_context.ts +++ b/src/core/server/core_context.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ConfigService, Env } from './config'; +import { IConfigService, Env } from './config'; import { LoggerFactory } from './logging'; /** @internal */ @@ -31,6 +31,6 @@ export type CoreId = symbol; export interface CoreContext { coreId: CoreId; env: Env; - configService: ConfigService; + configService: IConfigService; logger: LoggerFactory; } diff --git a/src/core/server/elasticsearch/retry_call_cluster.test.ts b/src/core/server/elasticsearch/retry_call_cluster.test.ts new file mode 100644 index 00000000000000..e2c6415a08c56d --- /dev/null +++ b/src/core/server/elasticsearch/retry_call_cluster.test.ts @@ -0,0 +1,58 @@ +/* + * 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 elasticsearch from 'elasticsearch'; +import { retryCallCluster } from './retry_call_cluster'; + +describe('retryCallCluster', () => { + it('retries ES API calls that rejects with NoConnection errors', () => { + expect.assertions(1); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + return i++ <= 2 + ? Promise.reject(new elasticsearch.errors.NoConnections()) + : Promise.resolve('success'); + }); + const retried = retryCallCluster(callEsApi); + return expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + }); + + it('rejects when ES API calls reject with other errors', async () => { + expect.assertions(3); + const callEsApi = jest.fn(); + let i = 0; + callEsApi.mockImplementation(() => { + i++; + + return i === 1 + ? Promise.reject(new Error('unknown error')) + : i === 2 + ? Promise.resolve('success') + : i === 3 || i === 4 + ? Promise.reject(new elasticsearch.errors.NoConnections()) + : i === 5 + ? Promise.reject(new Error('unknown error')) + : null; + }); + const retried = retryCallCluster(callEsApi); + await expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + await expect(retried('endpoint')).resolves.toMatchInlineSnapshot(`"success"`); + return expect(retried('endpoint')).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + }); +}); diff --git a/src/core/server/elasticsearch/retry_call_cluster.ts b/src/core/server/elasticsearch/retry_call_cluster.ts new file mode 100644 index 00000000000000..4b74dffebbef9d --- /dev/null +++ b/src/core/server/elasticsearch/retry_call_cluster.ts @@ -0,0 +1,58 @@ +/* + * 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 { retryWhen, concatMap } from 'rxjs/operators'; +import { defer, throwError, iif, timer } from 'rxjs'; +import elasticsearch from 'elasticsearch'; +import { CallAPIOptions } from '.'; + +/** + * Retries the provided Elasticsearch API call when a `NoConnections` error is + * encountered. The API call will be retried once a second, indefinitely, until + * a successful response or a different error is received. + * + * @param apiCaller + */ + +// TODO: Replace with APICaller from './scoped_cluster_client' once #46668 is merged +export function retryCallCluster( + apiCaller: ( + endpoint: string, + clientParams: Record, + options?: CallAPIOptions + ) => Promise +) { + return (endpoint: string, clientParams: Record = {}, options?: CallAPIOptions) => { + return defer(() => apiCaller(endpoint, clientParams, options)) + .pipe( + retryWhen(errors => + errors.pipe( + concatMap((error, i) => + iif( + () => error instanceof elasticsearch.errors.NoConnections, + timer(1000), + throwError(error) + ) + ) + ) + ) + ) + .toPromise(); + }; +} diff --git a/src/core/server/http/base_path_service.test.ts b/src/core/server/http/base_path_service.test.ts index ffbbe158cb2d4d..01790b7c77e064 100644 --- a/src/core/server/http/base_path_service.test.ts +++ b/src/core/server/http/base_path_service.test.ts @@ -22,6 +22,18 @@ import { KibanaRequest } from './router'; import { httpServerMock } from './http_server.mocks'; describe('BasePath', () => { + describe('serverBasePath', () => { + it('defaults to an empty string', () => { + const basePath = new BasePath(); + expect(basePath.serverBasePath).toBe(''); + }); + + it('returns the server base path', () => { + const basePath = new BasePath('/server'); + expect(basePath.serverBasePath).toBe('/server'); + }); + }); + describe('#get()', () => { it('returns base path associated with an incoming Legacy.Request request', () => { const request = httpServerMock.createRawRequest(); diff --git a/src/core/server/http/base_path_service.ts b/src/core/server/http/base_path_service.ts index 951463a2c9919f..916419cac212a1 100644 --- a/src/core/server/http/base_path_service.ts +++ b/src/core/server/http/base_path_service.ts @@ -20,18 +20,39 @@ import { ensureRawRequest, KibanaRequest, LegacyRequest } from './router'; import { modifyUrl } from '../../utils'; +/** + * Access or manipulate the Kibana base path + * + * @public + */ export class BasePath { private readonly basePathCache = new WeakMap(); - constructor(private readonly serverBasePath?: string) {} + /** + * returns the server's basePath + * + * See {@link BasePath.get} for getting the basePath value for a specific request + */ + public readonly serverBasePath: string; + + /** @internal */ + constructor(serverBasePath: string = '') { + this.serverBasePath = serverBasePath; + } + /** + * returns `basePath` value, specific for an incoming request. + */ public get = (request: KibanaRequest | LegacyRequest) => { const requestScopePath = this.basePathCache.get(ensureRawRequest(request)) || ''; - const serverBasePath = this.serverBasePath || ''; - return `${serverBasePath}${requestScopePath}`; + return `${this.serverBasePath}${requestScopePath}`; }; - // should work only for KibanaRequest as soon as spaces migrate to NP + /** + * sets `basePath` value, specific for an incoming request. + * + * @privateRemarks should work only for KibanaRequest as soon as spaces migrate to NP + */ public set = (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => { const rawRequest = ensureRawRequest(request); @@ -43,8 +64,11 @@ export class BasePath { this.basePathCache.set(rawRequest, requestSpecificBasePath); }; + /** + * returns a new `basePath` value, prefixed with passed `url`. + */ public prepend = (path: string): string => { - if (!this.serverBasePath) return path; + if (this.serverBasePath === '') return path; return modifyUrl(path, parts => { if (!parts.hostname && parts.pathname && parts.pathname.startsWith('/')) { parts.pathname = `${this.serverBasePath}${parts.pathname}`; @@ -52,8 +76,11 @@ export class BasePath { }); }; + /** + * returns a new `basePath` value, cleaned up from passed `url`. + */ public remove = (path: string): string => { - if (!this.serverBasePath) { + if (this.serverBasePath === '') { return path; } @@ -68,3 +95,11 @@ export class BasePath { return path; }; } + +/** + * Access or manipulate the Kibana base path + * + * {@link BasePath} + * @public + */ +export type IBasePath = Pick; diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index cb6906379c4ef3..b56fef5f65c2a9 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -26,7 +26,7 @@ import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; -import { KibanaRequest, LegacyRequest, ResponseHeaders, IRouter } from './router'; +import { ResponseHeaders, IRouter } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -34,7 +34,7 @@ import { import { SessionStorageFactory } from './session_storage'; import { AuthStateStorage, GetAuthState, IsAuthenticated } from './auth_state_storage'; import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; -import { BasePath } from './base_path_service'; +import { BasePath, IBasePath } from './base_path_service'; /** * Kibana HTTP Service provides own abstraction for work with HTTP stack. @@ -148,24 +148,8 @@ export interface HttpServerSetup { * @param handler {@link OnPostAuthHandler} - function to call. */ registerOnPostAuth: (handler: OnPostAuthHandler) => void; - basePath: { - /** - * returns `basePath` value, specific for an incoming request. - */ - get: (request: KibanaRequest | LegacyRequest) => string; - /** - * sets `basePath` value, specific for an incoming request. - */ - set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; - /** - * returns a new `basePath` value, prefixed with passed `url`. - */ - prepend: (url: string) => string; - /** - * returns a new `basePath` value, cleaned up from passed `url`. - */ - remove: (url: string) => string; - }; + /** {@link BasePath} */ + basePath: IBasePath; auth: { get: GetAuthState; isAuthenticated: IsAuthenticated; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index c5f920dcb360e1..c0658ae8d1e5c4 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -30,6 +30,7 @@ type ServiceSetupMockType = jest.Mocked & { }; const createBasePathMock = (): jest.Mocked => ({ + serverBasePath: '/mock-server-basepath', get: jest.fn(), set: jest.fn(), prepend: jest.fn(), diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index 5814991a2dd27c..0ac5ad92763537 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -83,8 +83,8 @@ export type HttpServiceSetup = Omit & { registerRouteHandlerContext: ( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: RequestHandlerContextProvider - ) => RequestHandlerContextContainer; + provider: RequestHandlerContextProvider + ) => RequestHandlerContextContainer; }; /** @public */ @@ -103,7 +103,7 @@ export class HttpService implements CoreService; + private requestHandlerContext?: RequestHandlerContextContainer; constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger; @@ -150,7 +150,7 @@ export class HttpService implements CoreService( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: RequestHandlerContextProvider + provider: RequestHandlerContextProvider ) => this.requestHandlerContext!.registerContext(pluginOpaqueId, contextName, provider), }; diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 895396b91eb465..4f83cd996deba8 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -58,3 +58,4 @@ export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; export { SessionStorageFactory, SessionStorage } from './session_storage'; export { SessionStorageCookieOptions } from './cookie_session_storage'; export * from './types'; +export { BasePath, IBasePath } from './base_path_service'; diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index a391b2e2e5d458..cade4ea4d4f2cc 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -16,30 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { IContextProvider, IContextContainer } from '../context'; -import { KibanaRequest, KibanaResponseFactory, KibanaResponse } from './router'; - -/** - * Parameters passed to the request handler function. - * @public - */ -export type RequestHandlerParams = [KibanaRequest, KibanaResponseFactory]; -/** - * Expected outcome the request handler function. - * @public - */ -export type RequestHandlerReturn = KibanaResponse; +import { IContextProvider, IContextContainer } from '../context'; +import { RequestHandler } from './router'; +import { RequestHandlerContext } from '..'; /** * An object that handles registration of http request context providers. * @public */ -export type RequestHandlerContextContainer = IContextContainer< - TContext, - RequestHandlerReturn | Promise, - RequestHandlerParams ->; +export type RequestHandlerContextContainer = IContextContainer>; /** * Context provider for request handler. @@ -47,8 +33,6 @@ export type RequestHandlerContextContainer = IContextContainer< * * @public */ -export type RequestHandlerContextProvider = IContextProvider< - TContext, - keyof TContext, - RequestHandlerParams ->; +export type RequestHandlerContextProvider< + TContextName extends keyof RequestHandlerContext +> = IContextProvider, TContextName>; diff --git a/src/core/server/index.test.mocks.ts b/src/core/server/index.test.mocks.ts index 9526a7d79ee43d..12cba7b29fc780 100644 --- a/src/core/server/index.test.mocks.ts +++ b/src/core/server/index.test.mocks.ts @@ -35,7 +35,11 @@ jest.doMock('./elasticsearch/elasticsearch_service', () => ({ ElasticsearchService: jest.fn(() => mockElasticsearchService), })); -export const mockLegacyService = { setup: jest.fn(), start: jest.fn(), stop: jest.fn() }; +export const mockLegacyService = { + setup: jest.fn().mockReturnValue({ uiExports: {} }), + start: jest.fn(), + stop: jest.fn(), +}; jest.mock('./legacy/legacy_service', () => ({ LegacyService: jest.fn(() => mockLegacyService), })); @@ -45,3 +49,9 @@ export const mockConfigService = configServiceMock.create(); jest.doMock('./config/config_service', () => ({ ConfigService: jest.fn(() => mockConfigService), })); + +import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; +export const mockSavedObjectsService = savedObjectsServiceMock.create(); +jest.doMock('./saved_objects/saved_objects_service', () => ({ + SavedObjectsService: jest.fn(() => mockSavedObjectsService), +})); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ef31804be62b2b..ca497e0f2d32d4 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -55,10 +55,17 @@ import { } from './http'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; +import { SavedObjectsServiceStart } from './saved_objects'; export { bootstrap } from './bootstrap'; export { ConfigPath, ConfigService } from './config'; -export { IContextContainer, IContextProvider, IContextHandler } from './context'; +export { + IContextContainer, + IContextProvider, + HandlerFunction, + HandlerContextType, + HandlerParameters, +} from './context'; export { CoreId } from './core_context'; export { CallAPIOptions, @@ -77,6 +84,8 @@ export { AuthResultParams, AuthStatus, AuthToolkit, + BasePath, + IBasePath, CustomHttpResponseOptions, GetAuthHeaders, GetAuthState, @@ -99,8 +108,6 @@ export { RequestHandler, RequestHandlerContextContainer, RequestHandlerContextProvider, - RequestHandlerParams, - RequestHandlerReturn, ResponseError, ResponseErrorAttributes, ResponseHeaders, @@ -150,7 +157,7 @@ export { SavedObjectsResolveImportErrorsOptions, SavedObjectsSchema, SavedObjectsSerializer, - SavedObjectsService, + SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from './saved_objects'; @@ -209,8 +216,8 @@ export interface CoreSetup { isTlsEnabled: HttpServiceSetup['isTlsEnabled']; registerRouteHandlerContext: ( name: T, - provider: RequestHandlerContextProvider - ) => RequestHandlerContextContainer; + provider: RequestHandlerContextProvider + ) => RequestHandlerContextContainer; createRouter: () => IRouter; }; } @@ -230,9 +237,11 @@ export interface InternalCoreSetup { } /** - * @public + * @internal */ -export interface InternalCoreStart {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface InternalCoreStart { + savedObjects: SavedObjectsServiceStart; +} export { ContextSetup, diff --git a/test/common/services/kibana_server/status.js b/src/core/server/kibana_config.ts similarity index 58% rename from test/common/services/kibana_server/status.js rename to src/core/server/kibana_config.ts index 3988bab185fcca..d46960289a8d01 100644 --- a/test/common/services/kibana_server/status.js +++ b/src/core/server/kibana_config.ts @@ -17,26 +17,18 @@ * under the License. */ -import { resolve as resolveUrl } from 'url'; +import { schema, TypeOf } from '@kbn/config-schema'; -import Wreck from '@hapi/wreck'; +export type KibanaConfigType = TypeOf; -const get = async url => { - const { payload } = await Wreck.get(url, { json: 'force' }); - return payload; +export const config = { + path: 'kibana', + schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + defaultAppId: schema.string({ defaultValue: 'home' }), + index: schema.string({ defaultValue: '.kibana' }), + disableWelcomeScreen: schema.boolean({ defaultValue: false }), + autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), + autocompleteTimeout: schema.duration({ defaultValue: 1000 }), + }), }; - -export class KibanaServerStatus { - constructor(kibanaServerUrl) { - this.kibanaServerUrl = kibanaServerUrl; - } - - async get() { - return await get(resolveUrl(this.kibanaServerUrl, './api/status')); - } - - async getOverallState() { - const status = await this.get(); - return status.status.overall.state; - } -} 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 f0c477e627460b..9a23b3b3b23b37 100644 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap @@ -1,53 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: cli args. cluster manager with base path proxy 1`] = ` -Object { - "basePath": true, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": true, - "repl": false, - "silent": false, - "watch": false, -} -`; - -exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager with base path proxy.: config. cluster manager with base path proxy 1`] = ` -Object { - "server": Object { - "autoListen": true, - }, -} -`; - -exports[`once LegacyService is set up in \`devClusterMaster\` mode creates ClusterManager without base path proxy.: cluster manager without base path proxy 1`] = ` -Array [ - Array [ - Object { - "basePath": false, - "dev": true, - "open": false, - "optimize": false, - "oss": false, - "quiet": false, - "repl": false, - "silent": true, - "watch": false, - }, - Object { - "server": Object { - "autoListen": true, - }, - }, - undefined, - ], -] -`; - -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 reconfigures logging configuration if new config is received.: applyLoggingConfiguration params 1`] = ` Array [ Array [ @@ -60,8 +12,6 @@ Array [ ] `; -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`] = ` Array [ Array [ diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 9d208445a0a1fc..cf72bb72079e9c 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -21,6 +21,14 @@ import { BehaviorSubject, throwError } from 'rxjs'; jest.mock('../../../legacy/server/kbn_server'); jest.mock('../../../cli/cluster/cluster_manager'); +jest.mock('./plugins/find_legacy_plugin_specs.ts', () => ({ + findLegacyPluginSpecs: (settings: Record) => ({ + pluginSpecs: [], + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: [], + }), +})); import { LegacyService } from '.'; // @ts-ignore: implicit any for JS file @@ -36,6 +44,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 { SavedObjectsServiceStart } from 'src/core/server/saved_objects/saved_objects_service'; +import { KibanaMigrator } from '../saved_objects/migrations'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -55,6 +65,7 @@ let setupDeps: { let startDeps: { core: { http: HttpServiceStart; + savedObjects: SavedObjectsServiceStart; plugins: PluginsServiceStart; }; plugins: Record; @@ -95,6 +106,9 @@ beforeEach(() => { http: { isListening: () => true, }, + savedObjects: { + migrator: {} as KibanaMigrator, + }, plugins: { contracts: new Map() }, }, plugins: {}, @@ -130,13 +144,15 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, { server: { autoListen: true } }, { setupDeps, startDeps, handledConfigPaths: ['foo.bar'], logger, - } + }, + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); const [mockKbnServer] = MockKbnServer.mock.instances; @@ -158,13 +174,15 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, { server: { autoListen: true } }, { setupDeps, startDeps, handledConfigPaths: ['foo.bar'], logger, - } + }, + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); const [mockKbnServer] = MockKbnServer.mock.instances; @@ -184,7 +202,9 @@ describe('once LegacyService is set up with connection info', () => { }); await legacyService.setup(setupDeps); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); + await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"something failed"` + ); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.listen).toHaveBeenCalled(); @@ -200,8 +220,12 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await legacyService.setup(setupDeps); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingSnapshot(); + await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"something failed"` + ); + await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Legacy service is not setup yet."` + ); expect(MockKbnServer).not.toHaveBeenCalled(); expect(MockClusterManager).not.toHaveBeenCalled(); @@ -285,13 +309,15 @@ describe('once LegacyService is set up without connection info', () => { test('creates legacy kbnServer with `autoListen: false`.', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( + { server: { autoListen: true } }, { server: { autoListen: true } }, { setupDeps, startDeps, handledConfigPaths: ['foo.bar'], logger, - } + }, + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); }); @@ -332,9 +358,9 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); - expect(MockClusterManager.create.mock.calls).toMatchSnapshot( - 'cluster manager without base path proxy' - ); + const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; + expect(cliArgs.basePath).toBe(false); + expect(basePathProxy).not.toBeDefined(); }); test('creates ClusterManager with base path proxy.', async () => { @@ -355,9 +381,8 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { expect(MockClusterManager.create).toBeCalledTimes(1); - const [[cliArgs, config, basePathProxy]] = MockClusterManager.create.mock.calls; - expect(cliArgs).toMatchSnapshot('cli args. cluster manager with base path proxy'); - expect(config).toMatchSnapshot('config. cluster manager with base path proxy'); + const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; + expect(cliArgs.basePath).toEqual(true); expect(basePathProxy).toBeInstanceOf(BasePathProxyServer); }); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 612c46cd3bbc54..268e5f553723ff 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -18,15 +18,18 @@ */ import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; -import { first, map, mergeMap, publishReplay, tap } from 'rxjs/operators'; +import { first, map, publishReplay, tap } from 'rxjs/operators'; import { CoreService } from '../../types'; -import { InternalCoreSetup, InternalCoreStart } from '../../server'; +import { InternalCoreSetup, InternalCoreStart } from '../'; +import { SavedObjectsLegacyUiExports } from '../types'; import { Config } from '../config'; import { CoreContext } from '../core_context'; import { DevConfig, DevConfigType } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http'; import { Logger } from '../logging'; import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; +import { findLegacyPluginSpecs } from './plugins'; +import { LegacyPluginSpec } from './plugins/find_legacy_plugin_specs'; interface LegacyKbnServer { applyLoggingConfiguration: (settings: Readonly>) => void; @@ -70,13 +73,30 @@ export interface LegacyServiceStartDeps { } /** @internal */ -export class LegacyService implements CoreService { +export interface LegacyServiceSetup { + pluginSpecs: LegacyPluginSpec[]; + uiExports: SavedObjectsLegacyUiExports; + pluginExtendedConfig: Config; +} + +/** @internal */ +export class LegacyService implements CoreService { private readonly log: Logger; private readonly devConfig$: Observable; private readonly httpConfig$: Observable; private kbnServer?: LegacyKbnServer; private configSubscription?: Subscription; private setupDeps?: LegacyServiceSetupDeps; + private update$: ConnectableObservable | undefined; + private legacyRawConfig: Config | undefined; + private legacyPlugins: + | { + pluginSpecs: LegacyPluginSpec[]; + disabledPluginSpecs: LegacyPluginSpec[]; + uiExports: SavedObjectsLegacyUiExports; + } + | undefined; + private settings: Record | undefined; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('legacy-service'); @@ -87,17 +107,11 @@ export class LegacyService implements CoreService { .atPath('server') .pipe(map(rawConfig => new HttpConfig(rawConfig, coreContext.env))); } + public async setup(setupDeps: LegacyServiceSetupDeps) { this.setupDeps = setupDeps; - } - public async start(startDeps: LegacyServiceStartDeps) { - const { setupDeps } = this; - if (!setupDeps) { - throw new Error('Legacy service is not setup yet.'); - } - this.log.debug('starting legacy service'); - const update$ = this.coreContext.configService.getConfig$().pipe( + this.update$ = this.coreContext.configService.getConfig$().pipe( tap(config => { if (this.kbnServer !== undefined) { this.kbnServer.applyLoggingConfiguration(config.toRaw()); @@ -107,21 +121,66 @@ export class LegacyService implements CoreService { publishReplay(1) ) as ConnectableObservable; - this.configSubscription = update$.connect(); + this.configSubscription = this.update$.connect(); - // Receive initial config and create kbnServer/ClusterManager. - this.kbnServer = await update$ + this.settings = await this.update$ .pipe( first(), - mergeMap(async config => { - if (this.coreContext.env.isDevClusterMaster) { - await this.createClusterManager(config); - return; - } - return await this.createKbnServer(config, setupDeps, startDeps); - }) + map(config => getLegacyRawConfig(config)) ) .toPromise(); + + const { + pluginSpecs, + pluginExtendedConfig, + disabledPluginSpecs, + uiExports, + } = await findLegacyPluginSpecs(this.settings, this.coreContext.logger); + + this.legacyPlugins = { + pluginSpecs, + disabledPluginSpecs, + uiExports, + }; + + this.legacyRawConfig = pluginExtendedConfig; + + // check for unknown uiExport types + if (uiExports.unknown && uiExports.unknown.length > 0) { + throw new Error( + `Unknown uiExport types: ${uiExports.unknown + .map(({ pluginSpec, type }) => `${type} from ${pluginSpec.getId()}`) + .join(', ')}` + ); + } + + return { + pluginSpecs, + uiExports, + pluginExtendedConfig, + }; + } + + public async start(startDeps: LegacyServiceStartDeps) { + const { setupDeps } = this; + if (!setupDeps || !this.legacyRawConfig || !this.legacyPlugins || !this.settings) { + throw new Error('Legacy service is not setup yet.'); + } + this.log.debug('starting legacy service'); + + // Receive initial config and create kbnServer/ClusterManager. + + if (this.coreContext.env.isDevClusterMaster) { + await this.createClusterManager(this.legacyRawConfig); + } else { + this.kbnServer = await this.createKbnServer( + this.settings, + this.legacyRawConfig, + setupDeps, + startDeps, + this.legacyPlugins + ); + } } public async stop() { @@ -151,24 +210,35 @@ export class LegacyService implements CoreService { require('../../../cli/cluster/cluster_manager').create( this.coreContext.env.cliArgs, - getLegacyRawConfig(config), + config, await basePathProxy$.toPromise() ); } private async createKbnServer( + settings: Record, config: Config, setupDeps: LegacyServiceSetupDeps, - startDeps: LegacyServiceStartDeps + startDeps: LegacyServiceStartDeps, + legacyPlugins: { + pluginSpecs: LegacyPluginSpec[]; + disabledPluginSpecs: LegacyPluginSpec[]; + uiExports: SavedObjectsLegacyUiExports; + } ) { // eslint-disable-next-line @typescript-eslint/no-var-requires const KbnServer = require('../../../legacy/server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer(getLegacyRawConfig(config), { - handledConfigPaths: await this.coreContext.configService.getUsedPaths(), - setupDeps, - startDeps, - logger: this.coreContext.logger, - }); + const kbnServer: LegacyKbnServer = new KbnServer( + settings, + config, + { + handledConfigPaths: await this.coreContext.configService.getUsedPaths(), + setupDeps, + startDeps, + logger: this.coreContext.logger, + }, + legacyPlugins + ); // The kbnWorkerType check is necessary to prevent the repl // from being started multiple times in different processes. diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts new file mode 100644 index 00000000000000..f1f4da8d0b4d75 --- /dev/null +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable, merge, forkJoin } from 'rxjs'; +import { toArray, tap, distinct, map } from 'rxjs/operators'; +import { + findPluginSpecs, + defaultConfig, + // @ts-ignore +} from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; +import { LoggerFactory } from '../../logging'; +import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; +import { Config } from '../../config'; + +export interface LegacyPluginPack { + getPath(): string; +} + +export interface LegacyPluginSpec { + getId: () => unknown; + getExpectedKibanaVersion: () => string; + getConfigPrefix: () => string; +} + +export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) { + const configToMutate: Config = defaultConfig(settings); + const { + pack$, + invalidDirectoryError$, + invalidPackError$, + otherError$, + deprecation$, + invalidVersionSpec$, + spec$, + disabledSpec$, + }: { + pack$: Observable; + invalidDirectoryError$: Observable<{ path: string }>; + invalidPackError$: Observable<{ path: string }>; + otherError$: Observable; + deprecation$: Observable; + invalidVersionSpec$: Observable; + spec$: Observable; + disabledSpec$: Observable; + } = findPluginSpecs(settings, configToMutate) as any; + + const logger = loggerFactory.get('legacy-plugins'); + + const log$ = merge( + pack$.pipe( + tap(definition => { + const path = definition.getPath(); + logger.debug(`Found plugin at ${path}`, { path }); + }) + ), + + invalidDirectoryError$.pipe( + tap(error => { + logger.warn(`Unable to scan directory for plugins "${error.path}"`, { + err: error, + dir: error.path, + }); + }) + ), + + invalidPackError$.pipe( + tap(error => { + logger.warn(`Skipping non-plugin directory at ${error.path}`, { + path: error.path, + }); + }) + ), + + otherError$.pipe( + tap(error => { + // rethrow unhandled errors, which will fail the server + throw error; + }) + ), + + invalidVersionSpec$.pipe( + map(spec => { + const name = spec.getId(); + const pluginVersion = spec.getExpectedKibanaVersion(); + // @ts-ignore + const kibanaVersion = settings.pkg.version; + return `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; + }), + distinct(), + tap(message => { + logger.warn(message); + }) + ), + + deprecation$.pipe( + tap(({ spec, message }) => { + const deprecationLogger = loggerFactory.get( + 'plugins', + spec.getConfigPrefix(), + 'config', + 'deprecation' + ); + deprecationLogger.warn(message); + }) + ) + ); + + const [disabledPluginSpecs, pluginSpecs] = await forkJoin( + disabledSpec$.pipe(toArray()), + spec$.pipe(toArray()), + log$.pipe(toArray()) + ).toPromise(); + + return { + disabledPluginSpecs, + pluginSpecs, + pluginExtendedConfig: configToMutate, + uiExports: collectLegacyUiExports(pluginSpecs), + }; +} diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts new file mode 100644 index 00000000000000..7c69546f0c4de3 --- /dev/null +++ b/src/core/server/legacy/plugins/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { findLegacyPluginSpecs } from './find_legacy_plugin_specs'; diff --git a/src/core/server/logging/index.ts b/src/core/server/logging/index.ts index cde85c2600ffc3..fd35ed39092b31 100644 --- a/src/core/server/logging/index.ts +++ b/src/core/server/logging/index.ts @@ -24,4 +24,4 @@ export { LogLevel } from './log_level'; /** @internal */ export { config, LoggingConfigType } from './logging_config'; /** @internal */ -export { LoggingService } from './logging_service'; +export { LoggingService, ILoggingService } from './logging_service'; diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts index d423e6b064e5fa..b5f522ca36a5f0 100644 --- a/src/core/server/logging/logging_service.mock.ts +++ b/src/core/server/logging/logging_service.mock.ts @@ -19,10 +19,9 @@ // Test helpers to simplify mocking logs and collecting all their outputs import { Logger } from './logger'; -import { LoggingService } from './logging_service'; +import { ILoggingService } from './logging_service'; import { LoggerFactory } from './logger_factory'; -type LoggingServiceContract = PublicMethodsOf; type MockedLogger = jest.Mocked; const createLoggingServiceMock = () => { @@ -36,7 +35,7 @@ const createLoggingServiceMock = () => { warn: jest.fn(), }; - const mocked: jest.Mocked = { + const mocked: jest.Mocked = { get: jest.fn(), asLoggerFactory: jest.fn(), upgrade: jest.fn(), @@ -65,7 +64,7 @@ const collectLoggingServiceMock = (loggerFactory: LoggerFactory) => { }; const clearLoggingServiceMock = (loggerFactory: LoggerFactory) => { - const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked; + const mockedLoggerFactory = (loggerFactory as unknown) as jest.Mocked; mockedLoggerFactory.get.mockClear(); mockedLoggerFactory.asLoggerFactory.mockClear(); mockedLoggerFactory.upgrade.mockClear(); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index e340e769ac20e8..ada02c3b6901ad 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -24,6 +24,7 @@ import { LoggerAdapter } from './logger_adapter'; import { LoggerFactory } from './logger_factory'; import { LoggingConfigType, LoggerConfigType, LoggingConfig } from './logging_config'; +export type ILoggingService = PublicMethodsOf; /** * Service that is responsible for maintaining loggers and logger appenders. * @internal diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 1a667d6978f1a7..674f8df33ee37b 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -30,3 +30,7 @@ export { getSortedObjectsForExport, SavedObjectsExportOptions } from './export'; export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serialization'; export { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; + +export { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects_service'; + +export { config } from './saved_objects_config'; diff --git a/src/core/server/saved_objects/mappings/index.ts b/src/core/server/saved_objects/mappings/index.ts index 0d3bfd00c415ec..15b0736ca5f1fe 100644 --- a/src/core/server/saved_objects/mappings/index.ts +++ b/src/core/server/saved_objects/mappings/index.ts @@ -17,4 +17,10 @@ * under the License. */ export { getTypes, getProperty, getRootProperties, getRootPropertiesObjects } from './lib'; -export { FieldMapping, MappingMeta, MappingProperties, IndexMapping } from './types'; +export { + FieldMapping, + MappingMeta, + MappingProperties, + IndexMapping, + SavedObjectsMapping, +} from './types'; diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index f0ec1e4a9dda27..8bb1a69d2eb132 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -42,6 +42,11 @@ export interface MappingProperties { [field: string]: FieldMapping; } +export interface SavedObjectsMapping { + pluginId: string; + properties: MappingProperties; +} + export interface MappingMeta { // A dictionary of key -> md5 hash (e.g. 'dashboard': '24234qdfa3aefa3wa') // with each key being a root-level mapping property, and each value being diff --git a/src/core/server/saved_objects/migrations/README.md b/src/core/server/saved_objects/migrations/README.md index 0b62d86172a521..91249024358ac7 100644 --- a/src/core/server/saved_objects/migrations/README.md +++ b/src/core/server/saved_objects/migrations/README.md @@ -98,9 +98,19 @@ If a plugin is disbled, all of its documents are retained in the Kibana index. T Kibana index migrations expose a few config settings which might be tweaked: -* `migrations.scrollDuration` - The [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-search-context) value used to read batches of documents from the source index. Defaults to `15m`. -* `migrations.batchSize` - The number of documents to read / transform / write at a time during index migrations -* `migrations.pollInterval` - How often, in milliseconds, secondary Kibana instances will poll to see if the primary Kibana instance has finished migrating the index. +* `migrations.scrollDuration` - The + [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html#scroll-search-context) + value used to read batches of documents from the source index. Defaults to + `15m`. +* `migrations.batchSize` - The number of documents to read / transform / write + at a time during index migrations +* `migrations.pollInterval` - How often, in milliseconds, secondary Kibana + instances will poll to see if the primary Kibana instance has finished + migrating the index. +* `migrations.skip` - Skip running migrations on startup (defaults to false). + This should only be used for running integration tests without a running + elasticsearch cluster. Note: even though migrations won't run on startup, + individual docs will still be migrated when read from ES. ## Example diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index eb6de3afc95a3e..38496a3503833c 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -20,6 +20,10 @@ import _ from 'lodash'; import { RawSavedObjectDoc } from '../../serialization'; import { DocumentMigrator } from './document_migrator'; +import { loggingServiceMock } from '../../../logging/logging_service.mock'; + +const mockLoggerFactory = loggingServiceMock.create(); +const mockLogger = mockLoggerFactory.get('mock logger'); describe('DocumentMigrator', () => { function testOpts() { @@ -27,7 +31,7 @@ describe('DocumentMigrator', () => { kibanaVersion: '25.2.3', migrations: {}, validateDoc: _.noop, - log: jest.fn(), + log: mockLogger, }; } @@ -474,7 +478,7 @@ describe('DocumentMigrator', () => { }); it('logs the document and transform that failed', () => { - const log = jest.fn(); + const log = mockLogger; const migrator = new DocumentMigrator({ ...testOpts(), migrations: { @@ -497,28 +501,26 @@ describe('DocumentMigrator', () => { expect('Did not throw').toEqual('But it should have!'); } catch (error) { expect(error.message).toMatch(/Dang diggity!/); - const warning = log.mock.calls.filter(([[level]]) => level === 'warning')[0][1]; + const warning = loggingServiceMock.collect(mockLoggerFactory).warn[0][0]; expect(warning).toContain(JSON.stringify(failedDoc)); expect(warning).toContain('dog:1.2.3'); } }); it('logs message in transform function', () => { - const logStash: string[] = []; const logTestMsg = '...said the joker to the thief'; const migrator = new DocumentMigrator({ ...testOpts(), migrations: { dog: { '1.2.3': (doc, log) => { - log!.info(logTestMsg); + log.info(logTestMsg); + log.warning(logTestMsg); return doc; }, }, }, - log: (path: string[], message: string) => { - logStash.push(message); - }, + log: mockLogger, }); const doc = { id: 'joker', @@ -527,7 +529,8 @@ describe('DocumentMigrator', () => { migrationVersion: {}, }; migrator.migrate(doc); - expect(logStash[0]).toEqual(logTestMsg); + expect(loggingServiceMock.collect(mockLoggerFactory).info[0][0]).toEqual(logTestMsg); + expect(loggingServiceMock.collect(mockLoggerFactory).warn[1][0]).toEqual(logTestMsg); }); test('extracts the latest migration version info', () => { diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 0576f1d22199fa..563d978dcc1f1e 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -64,26 +64,26 @@ import Boom from 'boom'; import _ from 'lodash'; import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; +import { Logger } from '../../../logging'; import { RawSavedObjectDoc } from '../../serialization'; import { SavedObjectsMigrationVersion } from '../../types'; -import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; +import { MigrationLogger, SavedObjectsMigrationLogger } from './migration_logger'; -export type TransformFn = ( - doc: RawSavedObjectDoc, - log?: SavedObjectsMigrationLogger -) => RawSavedObjectDoc; +export type TransformFn = (doc: RawSavedObjectDoc) => RawSavedObjectDoc; + +type MigrationFn = (doc: RawSavedObjectDoc, log: SavedObjectsMigrationLogger) => RawSavedObjectDoc; type ValidateDoc = (doc: RawSavedObjectDoc) => void; -interface MigrationDefinition { - [type: string]: { [version: string]: TransformFn }; +export interface MigrationDefinition { + [type: string]: { [version: string]: MigrationFn }; } interface Opts { kibanaVersion: string; migrations: MigrationDefinition; validateDoc: ValidateDoc; - log: LogFn; + log: Logger; } interface ActiveMigrations { @@ -125,7 +125,7 @@ export class DocumentMigrator implements VersionedTransformer { constructor(opts: Opts) { validateMigrationDefinition(opts.migrations); - this.migrations = buildActiveMigrations(opts.migrations, new MigrationLogger(opts.log)); + this.migrations = buildActiveMigrations(opts.migrations, opts.log); this.transformDoc = buildDocumentTransform({ kibanaVersion: opts.kibanaVersion, migrations: this.migrations, @@ -207,10 +207,7 @@ function validateMigrationDefinition(migrations: MigrationDefinition) { * From: { type: { version: fn } } * To: { type: { latestVersion: string, transforms: [{ version: string, transform: fn }] } } */ -function buildActiveMigrations( - migrations: MigrationDefinition, - log: SavedObjectsMigrationLogger -): ActiveMigrations { +function buildActiveMigrations(migrations: MigrationDefinition, log: Logger): ActiveMigrations { return _.mapValues(migrations, (versions, prop) => { const transforms = Object.entries(versions) .map(([version, transform]) => ({ @@ -299,15 +296,10 @@ function markAsUpToDate(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { * If a specific transform function fails, this tacks on a bit of information * about the document and transform that caused the failure. */ -function wrapWithTry( - version: string, - prop: string, - transform: TransformFn, - log: SavedObjectsMigrationLogger -) { +function wrapWithTry(version: string, prop: string, transform: MigrationFn, log: Logger) { return function tryTransformDoc(doc: RawSavedObjectDoc) { try { - const result = transform(doc, log); + const result = transform(doc, new MigrationLogger(log)); // A basic sanity check to help migration authors detect basic errors // (e.g. forgetting to return the transformed doc) @@ -319,7 +311,7 @@ function wrapWithTry( } catch (error) { const failedTransform = `${prop}:${version}`; const failedDoc = JSON.stringify(doc); - log.warning( + log.warn( `Failed to transform document ${doc}. Transform: ${failedTransform}\nDoc: ${failedDoc}` ); throw error; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index 2f9c5d1a08672a..2fc65f6e475d87 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -21,6 +21,7 @@ import _ from 'lodash'; import { SavedObjectsSchema } from '../../schema'; import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; import { IndexMigrator } from './index_migrator'; +import { loggingServiceMock } from '../../../logging/logging_service.mock'; describe('IndexMigrator', () => { let testOpts: any; @@ -30,7 +31,7 @@ describe('IndexMigrator', () => { batchSize: 10, callCluster: jest.fn(), index: '.kibana', - log: jest.fn(), + log: loggingServiceMock.create().get(), mappingProperties: {}, pollInterval: 1, scrollDuration: '1m', diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts index 633bccf8aceecb..d4e97ee6c57471 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -24,13 +24,14 @@ * serves as a central blueprint for what migrations will end up doing. */ +import { Logger } from 'src/core/server/logging'; import { SavedObjectsSerializer } from '../../serialization'; import { MappingProperties } from '../../mappings'; import { buildActiveMappings } from './build_active_mappings'; import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; import { fetchInfo, FullIndexInfo } from './elastic_index'; -import { LogFn, SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; +import { SavedObjectsMigrationLogger, MigrationLogger } from './migration_logger'; export interface MigrationOpts { batchSize: number; @@ -38,7 +39,7 @@ export interface MigrationOpts { scrollDuration: string; callCluster: CallCluster; index: string; - log: LogFn; + log: Logger; mappingProperties: MappingProperties; documentMigrator: VersionedTransformer; serializer: SavedObjectsSerializer; @@ -71,8 +72,7 @@ export interface Context { * and various info needed to migrate the source index. */ export async function migrationContext(opts: MigrationOpts): Promise { - const { callCluster } = opts; - const log = new MigrationLogger(opts.log); + const { log, callCluster } = opts; const alias = opts.index; const source = createSourceContext(await fetchInfo(callCluster, alias), alias); const dest = createDestContext(source, alias, opts.mappingProperties); @@ -82,7 +82,7 @@ export async function migrationContext(opts: MigrationOpts): Promise { alias, source, dest, - log, + log: new MigrationLogger(log), batchSize: opts.batchSize, documentMigrator: opts.documentMigrator, pollInterval: opts.pollInterval, diff --git a/src/core/server/saved_objects/migrations/core/migration_logger.ts b/src/core/server/saved_objects/migrations/core/migration_logger.ts index 9c98b7d85a8d82..7c61d0c48d9bd8 100644 --- a/src/core/server/saved_objects/migrations/core/migration_logger.ts +++ b/src/core/server/saved_objects/migrations/core/migration_logger.ts @@ -17,6 +17,8 @@ * under the License. */ +import { Logger } from 'src/core/server/logging'; + /* * This file provides a helper class for ensuring that all logging * in the migration system is done in a fairly uniform way. @@ -32,13 +34,13 @@ export interface SavedObjectsMigrationLogger { } export class MigrationLogger implements SavedObjectsMigrationLogger { - private log: LogFn; + private logger: Logger; - constructor(log: LogFn) { - this.log = log; + constructor(log: Logger) { + this.logger = log; } - public info = (msg: string) => this.log(['info', 'migrations'], msg); - public debug = (msg: string) => this.log(['debug', 'migrations'], msg); - public warning = (msg: string) => this.log(['warning', 'migrations'], msg); + public info = (msg: string) => this.logger.info(msg); + public debug = (msg: string) => this.logger.debug(msg); + public warning = (msg: string) => this.logger.warn(msg); } diff --git a/src/core/server/saved_objects/migrations/index.ts b/src/core/server/saved_objects/migrations/index.ts index 7a5c16bbe4af04..6cb7ecac92ab9e 100644 --- a/src/core/server/saved_objects/migrations/index.ts +++ b/src/core/server/saved_objects/migrations/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { KibanaMigrator } from './kibana'; +export { KibanaMigrator, IKibanaMigrator } from './kibana'; diff --git a/src/core/server/saved_objects/migrations/kibana/index.ts b/src/core/server/saved_objects/migrations/kibana/index.ts index ed48f3f4893de6..25772c4c9b0b16 100644 --- a/src/core/server/saved_objects/migrations/kibana/index.ts +++ b/src/core/server/saved_objects/migrations/kibana/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { KibanaMigrator } from './kibana_migrator'; +export { KibanaMigrator, IKibanaMigrator } from './kibana_migrator'; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts new file mode 100644 index 00000000000000..ca732f4f150282 --- /dev/null +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -0,0 +1,46 @@ +/* + * 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 { KibanaMigrator, mergeProperties } from './kibana_migrator'; +import { buildActiveMappings } from '../core'; +import { SavedObjectsMapping } from '../../mappings'; + +const createMigrator = ( + { + savedObjectMappings, + }: { + savedObjectMappings: SavedObjectsMapping[]; + } = { savedObjectMappings: [] } +) => { + const mockMigrator: jest.Mocked> = { + runMigrations: jest.fn(), + getActiveMappings: jest.fn(), + migrateDocument: jest.fn(), + }; + + mockMigrator.getActiveMappings.mockReturnValue( + buildActiveMappings({ properties: mergeProperties(savedObjectMappings) }) + ); + mockMigrator.migrateDocument.mockImplementation(doc => doc); + return mockMigrator; +}; + +export const mockKibanaMigrator = { + create: createMigrator, +}; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index 9fc8afd356043c..51551ae4887b51 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -18,13 +18,14 @@ */ import _ from 'lodash'; -import { KbnServer, KibanaMigrator } from './kibana_migrator'; +import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; +import { loggingServiceMock } from '../../../logging/logging_service.mock'; describe('KibanaMigrator', () => { describe('getActiveMappings', () => { it('returns full index mappings w/ core properties', () => { - const { kbnServer } = mockKbnServer(); - kbnServer.uiExports.savedObjectMappings = [ + const options = mockOptions(); + options.savedObjectMappings = [ { pluginId: 'aaa', properties: { amap: { type: 'text' } }, @@ -34,13 +35,13 @@ describe('KibanaMigrator', () => { properties: { bmap: { type: 'text' } }, }, ]; - const mappings = new KibanaMigrator({ kbnServer }).getActiveMappings(); + const mappings = new KibanaMigrator(options).getActiveMappings(); expect(mappings).toMatchSnapshot(); }); it('Fails if duplicate mappings are defined', () => { - const { kbnServer } = mockKbnServer(); - kbnServer.uiExports.savedObjectMappings = [ + const options = mockOptions(); + options.savedObjectMappings = [ { pluginId: 'aaa', properties: { amap: { type: 'text' } }, @@ -50,56 +51,27 @@ describe('KibanaMigrator', () => { properties: { amap: { type: 'long' } }, }, ]; - expect(() => new KibanaMigrator({ kbnServer }).getActiveMappings()).toThrow( + expect(() => new KibanaMigrator(options).getActiveMappings()).toThrow( /Plugin bbb is attempting to redefine mapping "amap"/ ); }); }); - describe('awaitMigration', () => { - it('changes isMigrated to true if migrations were skipped', async () => { - const { kbnServer } = mockKbnServer(); - kbnServer.server.plugins.elasticsearch = undefined; - const result = await new KibanaMigrator({ kbnServer }).awaitMigration(); + describe('runMigrations', () => { + it('resolves isMigrated if migrations were skipped', async () => { + const skipMigrations = true; + const result = await new KibanaMigrator(mockOptions()).runMigrations(skipMigrations); expect(result).toEqual([{ status: 'skipped' }, { status: 'skipped' }]); }); - it('waits for kbnServer.ready and elasticsearch.ready before attempting migrations', async () => { - const { kbnServer } = mockKbnServer(); + it('only runs migrations once if called multiple times', async () => { + const options = mockOptions(); const clusterStub = jest.fn(() => ({ status: 404 })); - const waitUntilReady = jest.fn(async () => undefined); - kbnServer.server.plugins.elasticsearch = { - waitUntilReady, - getCluster() { - expect(kbnServer.ready as any).toHaveBeenCalledTimes(1); - expect(waitUntilReady).toHaveBeenCalledTimes(1); - - return { - callWithInternalUser: clusterStub, - }; - }, - }; - - const migrationResults = await new KibanaMigrator({ kbnServer }).awaitMigration(); - expect(migrationResults.length).toEqual(2); - }); - - it('only handles and deletes index templates once', async () => { - const { kbnServer } = mockKbnServer(); - const clusterStub = jest.fn(() => ({ status: 404 })); - const waitUntilReady = jest.fn(async () => undefined); - - kbnServer.server.plugins.elasticsearch = { - waitUntilReady, - getCluster() { - return { - callWithInternalUser: clusterStub, - }; - }, - }; - - await new KibanaMigrator({ kbnServer }).awaitMigration(); + options.callCluster = clusterStub; + const migrator = new KibanaMigrator(options); + await migrator.runMigrations(); + await migrator.runMigrations(); // callCluster with "cat.templates" is called by "deleteIndexTemplates" function // and should only be done once @@ -111,75 +83,60 @@ describe('KibanaMigrator', () => { }); }); -function mockKbnServer({ configValues }: { configValues?: any } = {}) { +function mockOptions({ configValues }: { configValues?: any } = {}): KibanaMigratorOptions { const callCluster = jest.fn(); - const kbnServer: KbnServer = { - version: '8.2.3', - ready: jest.fn(async () => undefined), - uiExports: { - savedObjectsManagement: {}, - savedObjectValidations: {}, - savedObjectMigrations: {}, - savedObjectMappings: [ - { - pluginId: 'testtype', - properties: { - testtype: { - properties: { - name: { type: 'keyword' }, - }, + return { + logger: loggingServiceMock.create().get(), + kibanaVersion: '8.2.3', + savedObjectValidations: {}, + savedObjectMigrations: {}, + savedObjectMappings: [ + { + pluginId: 'testtype', + properties: { + testtype: { + properties: { + name: { type: 'keyword' }, }, }, }, - { - pluginId: 'testtype2', - properties: { - testtype2: { - properties: { - name: { type: 'keyword' }, - }, + }, + { + pluginId: 'testtype2', + properties: { + testtype2: { + properties: { + name: { type: 'keyword' }, }, }, }, - ], - savedObjectSchemas: { - testtype2: { - isNamespaceAgnostic: false, - indexPattern: 'other-index', - }, }, - }, - server: { - config: () => ({ - get: ((name: string) => { - if (configValues && configValues[name]) { - return configValues[name]; - } - switch (name) { - case 'kibana.index': - return '.my-index'; - case 'migrations.batchSize': - return 20; - case 'migrations.pollInterval': - return 20000; - case 'migrations.scrollDuration': - return '10m'; - default: - throw new Error(`Unexpected config ${name}`); - } - }) as any, - }), - log: _.noop as any, - plugins: { - elasticsearch: { - getCluster: () => ({ - callWithInternalUser: callCluster, - }), - waitUntilReady: async () => undefined, - }, + ], + savedObjectSchemas: { + testtype2: { + isNamespaceAgnostic: false, + indexPattern: 'other-index', }, }, + kibanaConfig: { + enabled: true, + index: '.my-index', + } as KibanaMigratorOptions['kibanaConfig'], + savedObjectsConfig: { + batchSize: 20, + pollInterval: 20000, + scrollDuration: '10m', + skip: false, + }, + config: { + get: (name: string) => { + if (configValues && configValues[name]) { + return configValues[name]; + } else { + throw new Error(`Unexpected config ${name}`); + } + }, + } as KibanaMigratorOptions['config'], + callCluster, }; - - return { kbnServer, callCluster }; } diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 78a8507e0c41d9..5bde5deec93820 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -22,81 +22,112 @@ * (the shape of the mappings and documents in the index). */ -import { once } from 'lodash'; -import { MappingProperties } from '../../mappings'; +import { Logger } from 'src/core/server/logging'; +import { KibanaConfigType } from 'src/core/server/kibana_config'; +import { MappingProperties, SavedObjectsMapping, IndexMapping } from '../../mappings'; import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from '../../schema'; -import { SavedObjectsManagementDefinition } from '../../management'; import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; -import { docValidator } from '../../validation'; -import { buildActiveMappings, CallCluster, IndexMigrator, LogFn } from '../core'; -import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; +import { docValidator, PropertyValidators } from '../../validation'; +import { buildActiveMappings, CallCluster, IndexMigrator } from '../core'; +import { + DocumentMigrator, + VersionedTransformer, + MigrationDefinition, +} from '../core/document_migrator'; import { createIndexMap } from '../core/build_index_map'; +import { SavedObjectsConfigType } from '../../saved_objects_config'; import { Config } from '../../../config'; -export interface KbnServer { - server: Server; - version: string; - ready: () => Promise; - uiExports: { - savedObjectMappings: any[]; - savedObjectMigrations: any; - savedObjectValidations: any; - savedObjectSchemas: SavedObjectsSchemaDefinition; - savedObjectsManagement: SavedObjectsManagementDefinition; - }; -} -interface Server { - log: LogFn; - config: () => { - get: { - (path: 'kibana.index' | 'migrations.scrollDuration'): string; - (path: 'migrations.batchSize' | 'migrations.pollInterval'): number; - }; - }; - plugins: { elasticsearch: ElasticsearchPlugin | undefined }; +export interface KibanaMigratorOptions { + callCluster: CallCluster; + config: Config; + savedObjectsConfig: SavedObjectsConfigType; + kibanaConfig: KibanaConfigType; + kibanaVersion: string; + logger: Logger; + savedObjectMappings: SavedObjectsMapping[]; + savedObjectMigrations: MigrationDefinition; + savedObjectSchemas: SavedObjectsSchemaDefinition; + savedObjectValidations: PropertyValidators; } -interface ElasticsearchPlugin { - getCluster: (name: 'admin') => { callWithInternalUser: CallCluster }; - waitUntilReady: () => Promise; -} +export type IKibanaMigrator = Pick; /** * Manages the shape of mappings and documents in the Kibana index. - * - * @export - * @class KibanaMigrator */ export class KibanaMigrator { + private readonly callCluster: CallCluster; + private readonly config: Config; + private readonly savedObjectsConfig: SavedObjectsConfigType; + private readonly documentMigrator: VersionedTransformer; + private readonly kibanaConfig: KibanaConfigType; + private readonly log: Logger; + private readonly mappingProperties: MappingProperties; + private readonly schema: SavedObjectsSchema; + private readonly serializer: SavedObjectsSerializer; + private migrationResult?: Promise>; + + /** + * Creates an instance of KibanaMigrator. + */ + constructor({ + callCluster, + config, + kibanaConfig, + savedObjectsConfig, + kibanaVersion, + logger, + savedObjectMappings, + savedObjectMigrations, + savedObjectSchemas, + savedObjectValidations, + }: KibanaMigratorOptions) { + this.config = config; + this.callCluster = callCluster; + this.kibanaConfig = kibanaConfig; + this.savedObjectsConfig = savedObjectsConfig; + this.schema = new SavedObjectsSchema(savedObjectSchemas); + this.serializer = new SavedObjectsSerializer(this.schema); + this.mappingProperties = mergeProperties(savedObjectMappings || []); + this.log = logger; + this.documentMigrator = new DocumentMigrator({ + kibanaVersion, + migrations: savedObjectMigrations || {}, + validateDoc: docValidator(savedObjectValidations || {}), + log: this.log, + }); + } + /** * Migrates the mappings and documents in the Kibana index. This will run only * once and subsequent calls will return the result of the original call. * - * @returns - * @memberof KibanaMigrator + * @returns - A promise which resolves once all migrations have been applied. + * The promise resolves with an array of migration statuses, one for each + * elasticsearch index which was migrated. */ - public awaitMigration = once(async () => { - const { server } = this.kbnServer; + public runMigrations(skipMigrations: boolean = false): Promise> { + if (this.migrationResult === undefined) { + this.migrationResult = this.runMigrationsInternal(skipMigrations); + } - // Wait until the plugins have been found an initialized... - await this.kbnServer.ready(); + return this.migrationResult; + } - // We can't do anything if the elasticsearch plugin has been disabled. - if (!server.plugins.elasticsearch) { - server.log( - ['warning', 'migration'], - 'The elasticsearch plugin is disabled. Skipping migrations.' + private runMigrationsInternal(skipMigrations: boolean) { + if (skipMigrations) { + this.log.warn( + 'Skipping Saved Object migrations on startup. Note: Individual documents will still be migrated when read or written.' + ); + return Promise.resolve( + Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })) ); - return Object.keys(this.mappingProperties).map(() => ({ status: 'skipped' })); } - // Wait until elasticsearch is green... - await server.plugins.elasticsearch.waitUntilReady(); - - const config = server.config() as Config; - const kibanaIndexName = config.get('kibana.index'); + const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ - config, + config: this.config, kibanaIndexName, indexMap: this.mappingProperties, schema: this.schema, @@ -104,14 +135,14 @@ export class KibanaMigrator { const migrators = Object.keys(indexMap).map(index => { return new IndexMigrator({ - batchSize: config.get('migrations.batchSize'), - callCluster: server.plugins.elasticsearch!.getCluster('admin').callWithInternalUser, + batchSize: this.savedObjectsConfig.batchSize, + callCluster: this.callCluster, documentMigrator: this.documentMigrator, index, log: this.log, mappingProperties: indexMap[index].typeMappings, - pollInterval: config.get('migrations.pollInterval'), - scrollDuration: config.get('migrations.scrollDuration'), + pollInterval: this.savedObjectsConfig.pollInterval, + scrollDuration: this.savedObjectsConfig.scrollDuration, serializer: this.serializer, // Only necessary for the migrator of the kibana index. obsoleteIndexTemplatePattern: @@ -120,61 +151,22 @@ export class KibanaMigrator { }); }); - if (migrators.length === 0) { - throw new Error(`Migrations failed to run, no mappings found or Kibana is not "ready".`); - } - return Promise.all(migrators.map(migrator => migrator.migrate())); - }); - - private kbnServer: KbnServer; - private documentMigrator: VersionedTransformer; - private mappingProperties: MappingProperties; - private log: LogFn; - private serializer: SavedObjectsSerializer; - private readonly schema: SavedObjectsSchema; - - /** - * Creates an instance of KibanaMigrator. - * - * @param opts - * @prop {KbnServer} kbnServer - An instance of the Kibana server object. - * @memberof KibanaMigrator - */ - constructor({ kbnServer }: { kbnServer: KbnServer }) { - this.kbnServer = kbnServer; - - this.schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas); - this.serializer = new SavedObjectsSerializer(this.schema); - - this.mappingProperties = mergeProperties(kbnServer.uiExports.savedObjectMappings || []); - - this.log = (meta: string[], message: string) => kbnServer.server.log(meta, message); - - this.documentMigrator = new DocumentMigrator({ - kibanaVersion: kbnServer.version, - migrations: kbnServer.uiExports.savedObjectMigrations || {}, - validateDoc: docValidator(kbnServer.uiExports.savedObjectValidations || {}), - log: this.log, - }); } /** * Gets all the index mappings defined by Kibana's enabled plugins. * - * @returns - * @memberof KibanaMigrator */ - public getActiveMappings() { + public getActiveMappings(): IndexMapping { return buildActiveMappings({ properties: this.mappingProperties }); } /** * Migrates an individual doc to the latest version, as defined by the plugin migrations. * - * @param {RawSavedObjectDoc} doc - * @returns {RawSavedObjectDoc} - * @memberof KibanaMigrator + * @param doc - The saved object to migrate + * @returns `doc` with all registered migrations applied. */ public migrateDocument(doc: RawSavedObjectDoc): RawSavedObjectDoc { return this.documentMigrator.migrate(doc); @@ -185,7 +177,7 @@ export class KibanaMigrator { * Merges savedObjectMappings properties into a single object, verifying that * no mappings are redefined. */ -function mergeProperties(mappings: any[]): MappingProperties { +export function mergeProperties(mappings: SavedObjectsMapping[]): MappingProperties { return mappings.reduce((acc, { pluginId, properties }) => { const duplicate = Object.keys(properties).find(k => acc.hasOwnProperty(k)); if (duplicate) { diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts new file mode 100644 index 00000000000000..7217cde55d0611 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export type SavedObjectsConfigType = TypeOf; + +export const config = { + path: 'migrations', + schema: schema.object({ + batchSize: schema.number({ defaultValue: 100 }), + scrollDuration: schema.string({ defaultValue: '15m' }), + pollInterval: schema.number({ defaultValue: 1500 }), + skip: schema.boolean({ defaultValue: false }), + }), +}; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts new file mode 100644 index 00000000000000..5561031d820ec0 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -0,0 +1,49 @@ +/* + * 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 { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects_service'; +import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; + +type SavedObjectsServiceContract = PublicMethodsOf; + +const createStartContractMock = () => { + const startContract: jest.Mocked = { + migrator: mockKibanaMigrator.create(), + }; + + return startContract; +}; + +const createsavedObjectsServiceMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockResolvedValue({}); + mocked.start.mockResolvedValue(createStartContractMock()); + mocked.stop.mockResolvedValue(); + return mocked; +}; + +export const savedObjectsServiceMock = { + create: createsavedObjectsServiceMock, + createStartContract: createStartContractMock, +}; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts new file mode 100644 index 00000000000000..c13be579c04bb7 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -0,0 +1,113 @@ +/* + * 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. + */ + +jest.mock('./migrations/kibana/kibana_migrator'); + +import { SavedObjectsService, SavedObjectsSetupDeps } from './saved_objects_service'; +import { mockCoreContext } from '../core_context.mock'; +import { KibanaMigrator } from './migrations/kibana/kibana_migrator'; +import { of } from 'rxjs'; +import elasticsearch from 'elasticsearch'; +import { Env } from '../config'; +import { configServiceMock } from '../mocks'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('SavedObjectsService', () => { + describe('#setup()', () => { + it('creates a KibanaMigrator which retries NoConnections errors from callAsInternalUser', async () => { + const coreContext = mockCoreContext.create(); + let i = 0; + const clusterClient = { + callAsInternalUser: jest + .fn() + .mockImplementation(() => + i++ <= 2 + ? Promise.reject(new elasticsearch.errors.NoConnections()) + : Promise.resolve('success') + ), + }; + + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of(clusterClient) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + + return expect((KibanaMigrator as jest.Mock).mock.calls[0][0].callCluster()).resolves.toMatch( + 'success' + ); + }); + }); + + describe('#start()', () => { + it('skips KibanaMigrator migrations when --optimize=true', async () => { + const coreContext = mockCoreContext.create({ + env: ({ cliArgs: { optimize: true }, packageInfo: { version: 'x.x.x' } } as unknown) as Env, + }); + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; + await soService.start({}); + expect(migrator.runMigrations).toHaveBeenCalledWith(true); + }); + + it('skips KibanaMigrator migrations when migrations.skip=true', async () => { + const configService = configServiceMock.create({ atPath: { skip: true } }); + const coreContext = mockCoreContext.create({ configService }); + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; + await soService.start({}); + expect(migrator.runMigrations).toHaveBeenCalledWith(true); + }); + + it('resolves with KibanaMigrator after waiting for migrations to complete', async () => { + const configService = configServiceMock.create({ atPath: { skip: false } }); + const coreContext = mockCoreContext.create({ configService }); + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + await soService.setup(coreSetup); + const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; + expect(migrator.runMigrations).toHaveBeenCalledTimes(0); + const startContract = await soService.start({}); + expect(startContract.migrator).toBeInstanceOf(KibanaMigrator); + expect(migrator.runMigrations).toHaveBeenCalledWith(false); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts new file mode 100644 index 00000000000000..ebcea8dc3b2750 --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -0,0 +1,126 @@ +/* + * 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 { CoreService } from 'src/core/types'; +import { first } from 'rxjs/operators'; +import { KibanaMigrator, IKibanaMigrator } from './migrations'; +import { CoreContext } from '../core_context'; +import { LegacyServiceSetup } from '../legacy/legacy_service'; +import { ElasticsearchServiceSetup } from '../elasticsearch'; +import { KibanaConfigType } from '../kibana_config'; +import { retryCallCluster } from '../elasticsearch/retry_call_cluster'; +import { SavedObjectsConfigType } from './saved_objects_config'; +import { Logger } from '..'; + +/** + * @public + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SavedObjectsServiceSetup {} + +/** + * @public + */ +export interface SavedObjectsServiceStart { + migrator: IKibanaMigrator; +} + +/** @internal */ +export interface SavedObjectsSetupDeps { + legacy: LegacyServiceSetup; + elasticsearch: ElasticsearchServiceSetup; +} + +/** @internal */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SavedObjectsStartDeps {} + +export class SavedObjectsService + implements CoreService { + private migrator: KibanaMigrator | undefined; + logger: Logger; + + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('savedobjects-service'); + } + + public async setup(coreSetup: SavedObjectsSetupDeps) { + this.logger.debug('Setting up SavedObjects service'); + + const { + savedObjectSchemas, + savedObjectMappings, + savedObjectMigrations, + savedObjectValidations, + } = await coreSetup.legacy.uiExports; + + const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(first()).toPromise(); + + const kibanaConfig = await this.coreContext.configService + .atPath('kibana') + .pipe(first()) + .toPromise(); + + const savedObjectsConfig = await this.coreContext.configService + .atPath('migrations') + .pipe(first()) + .toPromise(); + + this.migrator = new KibanaMigrator({ + savedObjectSchemas, + savedObjectMappings, + savedObjectMigrations, + savedObjectValidations, + logger: this.coreContext.logger.get('migrations'), + kibanaVersion: this.coreContext.env.packageInfo.version, + config: coreSetup.legacy.pluginExtendedConfig, + savedObjectsConfig, + kibanaConfig, + callCluster: retryCallCluster(adminClient.callAsInternalUser), + }); + + return ({} as any) as Promise; + } + + public async start(core: SavedObjectsStartDeps): Promise { + this.logger.debug('Starting SavedObjects service'); + + /** + * Note: We want to ensure that migrations have completed before + * continuing with further Core startup steps that might use SavedObjects + * such as running the legacy server, legacy plugins and allowing incoming + * HTTP requests. + * + * However, our build system optimize step and some tests depend on the + * HTTP server running without an Elasticsearch server being available. + * So, when the `migrations.skip` is true, we skip migrations altogether. + */ + const cliArgs = this.coreContext.env.cliArgs; + const savedObjectsConfig = await this.coreContext.configService + .atPath('migrations') + .pipe(first()) + .toPromise(); + const skipMigrations = cliArgs.optimize || savedObjectsConfig.skip; + await this.migrator!.runMigrations(skipMigrations); + + return { migrator: this.migrator! }; + } + + public async stop() {} +} diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 09676fb5040124..06d29bf7dcf326 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -26,10 +26,12 @@ interface SavedObjectsSchemaTypeDefinition { convertToAliasScript?: string; } +/** @internal */ export interface SavedObjectsSchemaDefinition { [key: string]: SavedObjectsSchemaTypeDefinition; } +/** @internal */ export class SavedObjectsSchema { private readonly definition?: SavedObjectsSchemaDefinition; constructor(schemaDefinition?: SavedObjectsSchemaDefinition) { diff --git a/src/core/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts index bd875db2001f4e..217ffe7129e94e 100644 --- a/src/core/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -77,6 +77,7 @@ function assertNonEmptyString(value: string, name: string) { } } +/** @internal */ export class SavedObjectsSerializer { private readonly schema: SavedObjectsSchema; diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 386539e755d9a7..dbf35ff4e134d7 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -26,9 +26,10 @@ import { SavedObjectsSchema } from '../schema'; import { SavedObjectsResolveImportErrorsOptions } from '../import/types'; /** - * @public + * @internal + * @deprecated */ -export interface SavedObjectsService { +export interface SavedObjectsLegacyService { // ATTENTION: these types are incomplete addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider< Request @@ -55,6 +56,7 @@ export { SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, SavedObjectsErrorHelpers, + SavedObjectsCacheIndexPatterns, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts new file mode 100644 index 00000000000000..e3aeca42d1cf07 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; + +const mockGetFieldsForWildcard = jest.fn(); +const mockIndexPatternsService: jest.Mock = jest.fn().mockImplementation(() => ({ + getFieldsForWildcard: mockGetFieldsForWildcard, + getFieldsForTimePattern: jest.fn(), +})); + +describe('SavedObjectsRepository', () => { + let cacheIndexPatterns: SavedObjectsCacheIndexPatterns; + + const fields = [ + { + aggregatable: true, + name: 'config.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'foo.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'bar.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'baz.type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'dashboard.otherField', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'hiddenType.someField', + searchable: true, + type: 'string', + }, + ]; + + beforeEach(() => { + cacheIndexPatterns = new SavedObjectsCacheIndexPatterns(); + jest.clearAllMocks(); + }); + + it('setIndexPatterns should return an error object when indexPatternsService is undefined', async () => { + try { + await cacheIndexPatterns.setIndexPatterns('test-index'); + } catch (error) { + expect(error.message).toMatch('indexPatternsService is not defined'); + } + }); + + it('setIndexPatterns should return an error object if getFieldsForWildcard is not defined', async () => { + mockGetFieldsForWildcard.mockImplementation(() => { + throw new Error('something happen'); + }); + try { + cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); + await cacheIndexPatterns.setIndexPatterns('test-index'); + } catch (error) { + expect(error.message).toMatch('Index Pattern Error - something happen'); + } + }); + + it('setIndexPatterns should return empty array when getFieldsForWildcard is returning null or undefined', async () => { + mockGetFieldsForWildcard.mockImplementation(() => null); + cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); + await cacheIndexPatterns.setIndexPatterns('test-index'); + expect(cacheIndexPatterns.getIndexPatterns()).toEqual(undefined); + }); + + it('setIndexPatterns should return index pattern when getFieldsForWildcard is returning fields', async () => { + mockGetFieldsForWildcard.mockImplementation(() => fields); + cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); + await cacheIndexPatterns.setIndexPatterns('test-index'); + expect(cacheIndexPatterns.getIndexPatterns()).toEqual({ fields, title: 'test-index' }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.ts new file mode 100644 index 00000000000000..e96cf996f504c3 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/cache_index_patterns.ts @@ -0,0 +1,82 @@ +/* + * 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 { FieldDescriptor } from 'src/legacy/server/index_patterns/service/index_patterns_service'; +import { IndexPatternsService } from 'src/legacy/server/index_patterns'; + +export interface SavedObjectsIndexPatternField { + name: string; + type: string; + aggregatable: boolean; + searchable: boolean; +} + +export interface SavedObjectsIndexPattern { + fields: SavedObjectsIndexPatternField[]; + title: string; +} + +export class SavedObjectsCacheIndexPatterns { + private _indexPatterns: SavedObjectsIndexPattern | undefined = undefined; + private _indexPatternsService: IndexPatternsService | undefined = undefined; + + public setIndexPatternsService(indexPatternsService: IndexPatternsService) { + this._indexPatternsService = indexPatternsService; + } + + public getIndexPatternsService() { + return this._indexPatternsService; + } + + public getIndexPatterns(): SavedObjectsIndexPattern | undefined { + return this._indexPatterns; + } + + public async setIndexPatterns(index: string) { + await this._getIndexPattern(index); + } + + private async _getIndexPattern(index: string) { + try { + if (this._indexPatternsService == null) { + throw new TypeError('indexPatternsService is not defined'); + } + const fieldsDescriptor: FieldDescriptor[] = await this._indexPatternsService.getFieldsForWildcard( + { + pattern: index, + } + ); + + this._indexPatterns = + fieldsDescriptor && Array.isArray(fieldsDescriptor) && fieldsDescriptor.length > 0 + ? { + fields: fieldsDescriptor.map(field => ({ + aggregatable: field.aggregatable, + name: field.name, + searchable: field.searchable, + type: field.type, + })), + title: index, + } + : undefined; + } catch (err) { + throw new Error(`Index Pattern Error - ${err.message}`); + } + } +} diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts new file mode 100644 index 00000000000000..73a0804512ed10 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -0,0 +1,457 @@ +/* + * 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 { fromKueryExpression } from '@kbn/es-query'; + +import { + validateFilterKueryNode, + getSavedObjectTypeIndexPatterns, + validateConvertFilterToKueryNode, +} from './filter_utils'; +import { SavedObjectsIndexPattern } from './cache_index_patterns'; + +const mockIndexPatterns: SavedObjectsIndexPattern = { + fields: [ + { + name: 'updatedAt', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.title', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'bar.foo', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'bar.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'hiddentype.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + ], + title: 'mock', +}; + +describe('Filter Utils', () => { + describe('#validateConvertFilterToKueryNode', () => { + test('Validate a simple filter', () => { + expect( + validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockIndexPatterns) + ).toEqual(fromKueryExpression('foo.title: "best"')); + }); + test('Assemble filter kuery node saved object attributes with one saved object type', () => { + expect( + validateConvertFilterToKueryNode( + ['foo'], + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', + mockIndexPatterns + ) + ).toEqual( + fromKueryExpression( + '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' + ) + ); + }); + + test('Assemble filter with one type kuery node saved object attributes with multiple saved object type', () => { + expect( + validateConvertFilterToKueryNode( + ['foo', 'bar'], + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', + mockIndexPatterns + ) + ).toEqual( + fromKueryExpression( + '(type: foo and updatedAt: 5678654567) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or foo.description :*)' + ) + ); + }); + + test('Assemble filter with two types kuery node saved object attributes with multiple saved object type', () => { + expect( + validateConvertFilterToKueryNode( + ['foo', 'bar'], + '(bar.updatedAt: 5678654567 OR foo.updatedAt: 5678654567) and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or bar.attributes.description :*)', + mockIndexPatterns + ) + ).toEqual( + fromKueryExpression( + '((type: bar and updatedAt: 5678654567) or (type: foo and updatedAt: 5678654567)) and foo.bytes > 1000 and foo.bytes < 8000 and foo.title: "best" and (foo.description: t* or bar.description :*)' + ) + ); + }); + + test('Lets make sure that we are throwing an exception if we get an error', () => { + expect(() => { + validateConvertFilterToKueryNode( + ['foo', 'bar'], + 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', + mockIndexPatterns + ); + }).toThrowErrorMatchingInlineSnapshot( + `"This key 'updatedAt' need to be wrapped by a saved object type like foo,bar: Bad Request"` + ); + }); + + test('Lets make sure that we are throwing an exception if we are using hiddentype with types', () => { + expect(() => { + validateConvertFilterToKueryNode([], 'hiddentype.title: "title"', mockIndexPatterns); + }).toThrowErrorMatchingInlineSnapshot(`"This type hiddentype is not allowed: Bad Request"`); + }); + }); + + describe('#validateFilterKueryNode', () => { + test('Validate filter query through KueryNode - happy path', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updatedAt', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if key is not wrapper by a saved object type', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: "This key 'updatedAt' need to be wrapped by a saved object type like foo", + isSavedObjectAttr: true, + key: 'updatedAt', + type: null, + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if key of a saved object type is not wrapped with attributes', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updatedAt', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: + "This key 'foo.bytes' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: + "This key 'foo.description' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if filter is not using an allowed type', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: 'This type bar is not allowed', + isSavedObjectAttr: true, + key: 'bar.updatedAt', + type: 'bar', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { + const validationObject = validateFilterKueryNode( + fromKueryExpression( + 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + ['foo'], + getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + ); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: "This key 'foo.updatedAt33' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.updatedAt33', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.0', + error: + "This key 'foo.attributes.header' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.attributes.header', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1.arguments.1.arguments.1.arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + }); + + describe('#getSavedObjectTypeIndexPatterns', () => { + test('Get index patterns related to your type', () => { + const indexPatternsFilterByType = getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns); + + expect(indexPatternsFilterByType).toEqual([ + { + name: 'updatedAt', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.title', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.description', + type: 'text', + aggregatable: true, + searchable: true, + }, + { + name: 'foo.bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ]); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts new file mode 100644 index 00000000000000..2397971e66966f --- /dev/null +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -0,0 +1,190 @@ +/* + * 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 { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query'; +import { get, set } from 'lodash'; + +import { SavedObjectsIndexPattern, SavedObjectsIndexPatternField } from './cache_index_patterns'; +import { SavedObjectsErrorHelpers } from './errors'; + +export const validateConvertFilterToKueryNode = ( + types: string[], + filter: string, + indexPattern: SavedObjectsIndexPattern | undefined +): KueryNode => { + if (filter && filter.length > 0 && indexPattern) { + const filterKueryNode = fromKueryExpression(filter); + + const typeIndexPatterns = getSavedObjectTypeIndexPatterns(types, indexPattern); + const validationFilterKuery = validateFilterKueryNode( + filterKueryNode, + types, + typeIndexPatterns, + filterKueryNode.type === 'function' && ['is', 'range'].includes(filterKueryNode.function) + ); + + if (validationFilterKuery.length === 0) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'If we have a filter options defined, we should always have validationFilterKuery defined too' + ); + } + + if (validationFilterKuery.some(obj => obj.error != null)) { + throw SavedObjectsErrorHelpers.createBadRequestError( + validationFilterKuery + .filter(obj => obj.error != null) + .map(obj => obj.error) + .join('\n') + ); + } + + validationFilterKuery.forEach(item => { + const path: string[] = item.astPath.length === 0 ? [] : item.astPath.split('.'); + const existingKueryNode: KueryNode = + path.length === 0 ? filterKueryNode : get(filterKueryNode, path); + if (item.isSavedObjectAttr) { + existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; + const itemType = types.filter(t => t === item.type); + if (itemType.length === 1) { + set( + filterKueryNode, + path, + nodeTypes.function.buildNode('and', [ + nodeTypes.function.buildNode('is', 'type', itemType[0]), + existingKueryNode, + ]) + ); + } + } else { + existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.replace( + '.attributes', + '' + ); + set(filterKueryNode, path, existingKueryNode); + } + }); + return filterKueryNode; + } + return null; +}; + +export const getSavedObjectTypeIndexPatterns = ( + types: string[], + indexPattern: SavedObjectsIndexPattern | undefined +): SavedObjectsIndexPatternField[] => { + return indexPattern != null + ? indexPattern.fields.filter( + ip => + !ip.name.includes('.') || (ip.name.includes('.') && types.includes(ip.name.split('.')[0])) + ) + : []; +}; + +interface ValidateFilterKueryNode { + astPath: string; + error: string; + isSavedObjectAttr: boolean; + key: string; + type: string | null; +} + +export const validateFilterKueryNode = ( + astFilter: KueryNode, + types: string[], + typeIndexPatterns: SavedObjectsIndexPatternField[], + storeValue: boolean = false, + path: string = 'arguments' +): ValidateFilterKueryNode[] => { + return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode( + ast, + types, + typeIndexPatterns, + ast.type === 'function' && ['is', 'range'].includes(ast.function), + `${myPath}.arguments` + ), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + return [ + ...kueryNode, + { + astPath: splitPath.slice(0, splitPath.length - 1).join('.'), + error: hasFilterKeyError(ast.value, types, typeIndexPatterns), + isSavedObjectAttr: isSavedObjectAttr(ast.value, typeIndexPatterns), + key: ast.value, + type: getType(ast.value), + }, + ]; + } + return kueryNode; + }, []); +}; + +const getType = (key: string) => (key.includes('.') ? key.split('.')[0] : null); + +export const isSavedObjectAttr = ( + key: string, + typeIndexPatterns: SavedObjectsIndexPatternField[] +) => { + const splitKey = key.split('.'); + if (splitKey.length === 1 && typeIndexPatterns.some(tip => tip.name === splitKey[0])) { + return true; + } else if (splitKey.length > 1 && typeIndexPatterns.some(tip => tip.name === splitKey[1])) { + return true; + } + return false; +}; + +export const hasFilterKeyError = ( + key: string, + types: string[], + typeIndexPatterns: SavedObjectsIndexPatternField[] +): string | null => { + if (!key.includes('.')) { + return `This key '${key}' need to be wrapped by a saved object type like ${types.join()}`; + } else if (key.includes('.')) { + const keySplit = key.split('.'); + if (keySplit.length <= 1 || !types.includes(keySplit[0])) { + return `This type ${keySplit[0]} is not allowed`; + } + if ( + (keySplit.length === 2 && typeIndexPatterns.some(tip => tip.name === key)) || + (keySplit.length > 2 && types.includes(keySplit[0]) && keySplit[1] !== 'attributes') + ) { + return `This key '${key}' does NOT match the filter proposition SavedObjectType.attributes.key`; + } + if ( + (keySplit.length === 2 && !typeIndexPatterns.some(tip => tip.name === keySplit[1])) || + (keySplit.length > 2 && + !typeIndexPatterns.some( + tip => + tip.name === [...keySplit.slice(0, 1), ...keySplit.slice(2, keySplit.length)].join('.') + )) + ) { + return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`; + } + } + return null; +}; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index d987737c2ffa09..be78fdde762106 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -26,3 +26,5 @@ export { } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; + +export { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 5a2e6a617fbb5d..bc646c8c1d2e14 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -18,6 +18,7 @@ */ import { delay } from 'bluebird'; + import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; import { SavedObjectsErrorHelpers } from './errors'; @@ -263,7 +264,7 @@ describe('SavedObjectsRepository', () => { onBeforeWrite = jest.fn(); migrator = { migrateDocument: jest.fn(doc => doc), - awaitMigration: async () => ({ status: 'skipped' }), + runMigrations: async () => ({ status: 'skipped' }), }; const serializer = new SavedObjectsSerializer(schema); @@ -272,6 +273,10 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', + cacheIndexPatterns: { + setIndexPatterns: jest.fn(), + getIndexPatterns: () => undefined, + }, mappings, callCluster: callAdminCluster, migrator, @@ -285,7 +290,7 @@ describe('SavedObjectsRepository', () => { getSearchDslNS.getSearchDsl.mockReset(); }); - afterEach(() => {}); + afterEach(() => { }); describe('#create', () => { beforeEach(() => { @@ -297,7 +302,7 @@ describe('SavedObjectsRepository', () => { }); it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -313,7 +318,7 @@ describe('SavedObjectsRepository', () => { } ) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('formats Elasticsearch response', async () => { @@ -552,7 +557,7 @@ describe('SavedObjectsRepository', () => { describe('#bulkCreate', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); callAdminCluster.mockReturnValue({ @@ -576,7 +581,7 @@ describe('SavedObjectsRepository', () => { ]) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('formats Elasticsearch request', async () => { @@ -993,12 +998,12 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); - it('should return objects in the same order regardless of type', () => {}); + it('should return objects in the same order regardless of type', () => { }); }); describe('#delete', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); callAdminCluster.mockReturnValue({ result: 'deleted' }); @@ -1008,7 +1013,7 @@ describe('SavedObjectsRepository', () => { }) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('throws notFound when ES is unable to find the document', async () => { @@ -1114,14 +1119,14 @@ describe('SavedObjectsRepository', () => { describe('#find', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); callAdminCluster.mockReturnValue(noNamespaceSearchResults); await expect(savedObjectsRepository.find({ type: 'foo' })).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('requires type to be defined', async () => { @@ -1154,6 +1159,13 @@ describe('SavedObjectsRepository', () => { } }); + it('requires index pattern to be defined if filter is defined', async () => { + callAdminCluster.mockReturnValue(noNamespaceSearchResults); + expect(savedObjectsRepository.find({ type: 'foo', filter: 'foo.type: hello' })) + .rejects + .toThrowErrorMatchingInlineSnapshot('"options.filter is missing index pattern to work correctly: Bad Request"'); + }); + it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl', async () => { callAdminCluster.mockReturnValue(namespacedSearchResults); @@ -1169,6 +1181,8 @@ describe('SavedObjectsRepository', () => { type: 'foo', id: '1', }, + indexPattern: undefined, + kueryNode: null, }; await savedObjectsRepository.find(relevantOpts); @@ -1315,7 +1329,7 @@ describe('SavedObjectsRepository', () => { }; it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -1324,7 +1338,7 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository.get('index-pattern', 'logstash-*') ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('formats Elasticsearch response when there is no namespace', async () => { @@ -1408,7 +1422,7 @@ describe('SavedObjectsRepository', () => { describe('#bulkGet', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -1421,7 +1435,7 @@ describe('SavedObjectsRepository', () => { ]) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveBeenCalledTimes(1); + expect(migrator.runMigrations).toHaveBeenCalledTimes(1); }); it('prepends type to id when getting objects when there is no namespace', async () => { @@ -1662,7 +1676,7 @@ describe('SavedObjectsRepository', () => { }); it('waits until migrations are complete before proceeding', async () => { - migrator.awaitMigration = jest.fn(async () => + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled() ); @@ -1672,7 +1686,7 @@ describe('SavedObjectsRepository', () => { }) ).resolves.toBeDefined(); - expect(migrator.awaitMigration).toHaveReturnedTimes(1); + expect(migrator.runMigrations).toHaveReturnedTimes(1); }); it('mockReturnValue current ES document _seq_no and _primary_term encoded as version', async () => { diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e93d9e4047501e..aadb82486cccec 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -19,11 +19,13 @@ import { omit } from 'lodash'; import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; + import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { decorateEsError } from './decorate_es_error'; import { SavedObjectsErrorHelpers } from './errors'; +import { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; @@ -45,6 +47,7 @@ import { SavedObjectsFindOptions, SavedObjectsMigrationVersion, } from '../../types'; +import { validateConvertFilterToKueryNode } from './filter_utils'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -74,6 +77,7 @@ export interface SavedObjectsRepositoryOptions { serializer: SavedObjectsSerializer; migrator: KibanaMigrator; allowedTypes: string[]; + cacheIndexPatterns: SavedObjectsCacheIndexPatterns; onBeforeWrite?: (...args: Parameters) => Promise; } @@ -91,11 +95,13 @@ export class SavedObjectsRepository { private _onBeforeWrite: (...args: Parameters) => Promise; private _unwrappedCallCluster: CallCluster; private _serializer: SavedObjectsSerializer; + private _cacheIndexPatterns: SavedObjectsCacheIndexPatterns; constructor(options: SavedObjectsRepositoryOptions) { const { index, config, + cacheIndexPatterns, mappings, callCluster, schema, @@ -106,7 +112,7 @@ export class SavedObjectsRepository { } = options; // It's important that we migrate documents / mark them as up-to-date - // prior to writing them to the index. Otherwise, we'll cause unecessary + // prior to writing them to the index. Otherwise, we'll cause unnecessary // index migrations to run at Kibana startup, and those will probably fail // due to invalidly versioned documents in the index. // @@ -117,6 +123,7 @@ export class SavedObjectsRepository { this._config = config; this._mappings = mappings; this._schema = schema; + this._cacheIndexPatterns = cacheIndexPatterns; if (allowedTypes.length === 0) { throw new Error('Empty or missing types for saved object repository!'); } @@ -125,7 +132,10 @@ export class SavedObjectsRepository { this._onBeforeWrite = onBeforeWrite; this._unwrappedCallCluster = async (...args: Parameters) => { - await migrator.awaitMigration(); + await migrator.runMigrations(); + if (this._cacheIndexPatterns.getIndexPatterns() == null) { + await this._cacheIndexPatterns.setIndexPatterns(index); + } return callCluster(...args); }; this._schema = schema; @@ -404,9 +414,12 @@ export class SavedObjectsRepository { fields, namespace, type, + filter, }: SavedObjectsFindOptions): Promise> { if (!type) { - throw new TypeError(`options.type must be a string or an array of strings`); + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.type must be a string or an array of strings' + ); } const types = Array.isArray(type) ? type : [type]; @@ -421,13 +434,28 @@ export class SavedObjectsRepository { } if (searchFields && !Array.isArray(searchFields)) { - throw new TypeError('options.searchFields must be an array'); + throw SavedObjectsErrorHelpers.createBadRequestError('options.searchFields must be an array'); } if (fields && !Array.isArray(fields)) { - throw new TypeError('options.fields must be an array'); + throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array'); } + if (filter && filter !== '' && this._cacheIndexPatterns.getIndexPatterns() == null) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.filter is missing index pattern to work correctly' + ); + } + + const kueryNode = + filter && filter !== '' + ? validateConvertFilterToKueryNode( + allowedTypes, + filter, + this._cacheIndexPatterns.getIndexPatterns() + ) + : null; + const esOptions = { index: this.getIndicesForTypes(allowedTypes), size: perPage, @@ -446,6 +474,8 @@ export class SavedObjectsRepository { sortOrder, namespace, hasReference, + indexPattern: kueryNode != null ? this._cacheIndexPatterns.getIndexPatterns() : undefined, + kueryNode, }), }, }; @@ -769,7 +799,7 @@ export class SavedObjectsRepository { // The internal representation of the saved object that the serializer returns // includes the namespace, and we use this for migrating documents. However, we don't - // want the namespcae to be returned from the repository, as the repository scopes each + // want the namespace to be returned from the repository, as the repository scopes each // method transparently to the specified namespace. private _rawToSavedObject(raw: RawDoc): SavedObject { const savedObject = this._serializer.rawToSavedObject(raw); diff --git a/src/core/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 index b13d86819716be..75b30580292279 100644 --- a/src/core/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 @@ -18,6 +18,7 @@ */ import { schemaMock } from '../../../schema/schema.mock'; +import { SavedObjectsIndexPattern } from '../cache_index_patterns'; import { getQueryParams } from './query_params'; const SCHEMA = schemaMock.create(); @@ -61,6 +62,41 @@ const MAPPINGS = { }, }, }; +const INDEX_PATTERN: SavedObjectsIndexPattern = { + fields: [ + { + aggregatable: true, + name: 'type', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'pending.title', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'saved.title', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'saved.obj.key1', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'global.name', + searchable: true, + type: 'string', + }, + ], + title: 'test', +}; // create a type clause to be used within the "should", if a namespace is specified // the clause will ensure the namespace matches; otherwise, the clause will ensure @@ -85,7 +121,7 @@ const createTypeClause = (type: string, namespace?: string) => { describe('searchDsl/queryParams', () => { describe('no parameters', () => { it('searches for all known types without a namespace specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA)).toEqual({ + expect(getQueryParams({ mappings: MAPPINGS, schema: SCHEMA })).toEqual({ query: { bool: { filter: [ @@ -108,7 +144,9 @@ describe('searchDsl/queryParams', () => { describe('namespace', () => { it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace')).toEqual({ + expect( + getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: 'foo-namespace' }) + ).toEqual({ query: { bool: { filter: [ @@ -131,7 +169,9 @@ describe('searchDsl/queryParams', () => { describe('type (singular, namespaced)', () => { it('includes a terms filter for type and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'saved')).toEqual({ + expect( + getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'saved' }) + ).toEqual({ query: { bool: { filter: [ @@ -150,7 +190,9 @@ describe('searchDsl/queryParams', () => { describe('type (singular, global)', () => { it('includes a terms filter for type and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, 'global')).toEqual({ + expect( + getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'global' }) + ).toEqual({ query: { bool: { filter: [ @@ -169,7 +211,14 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global)', () => { it('includes term filters for types and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -188,7 +237,14 @@ describe('searchDsl/queryParams', () => { describe('namespace, type (plural, namespaced and global)', () => { it('includes a terms filter for type and namespace not being specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -207,7 +263,15 @@ describe('searchDsl/queryParams', () => { describe('search', () => { it('includes a sqs query and all known types without a namespace specified', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'us*')).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'us*', + }) + ).toEqual({ query: { bool: { filter: [ @@ -239,7 +303,15 @@ describe('searchDsl/queryParams', () => { describe('namespace, search', () => { it('includes a sqs query and namespaced types with the namespace and global types without a namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'us*')).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'us*', + }) + ).toEqual({ query: { bool: { filter: [ @@ -271,7 +343,15 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), search', () => { it('includes a sqs query and types without a namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'us*')).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'us*', + }) + ).toEqual({ query: { bool: { filter: [ @@ -299,40 +379,52 @@ describe('searchDsl/queryParams', () => { describe('namespace, type (plural, namespaced and global), search', () => { it('includes a sqs query and namespace type with a namespace and global type without a namespace', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'us*')).toEqual( - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'us*', + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [createTypeClause('saved', 'foo-namespace'), createTypeClause('global')], + minimum_should_match: 1, }, - ], - must: [ - { - simple_query_string: { - query: 'us*', - lenient: true, - fields: ['*'], - }, + }, + ], + must: [ + { + simple_query_string: { + query: 'us*', + lenient: true, + fields: ['*'], }, - ], - }, + }, + ], }, - } - ); + }, + }); }); }); describe('search, searchFields', () => { it('includes all types for field', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'y*', + searchFields: ['title'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -360,7 +452,16 @@ describe('searchDsl/queryParams', () => { }); }); it('supports field boosting', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title^3'])).toEqual({ + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'y*', + searchFields: ['title^3'], + }) + ).toEqual({ query: { bool: { filter: [ @@ -389,7 +490,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, undefined, 'y*', ['title', 'title.raw']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: undefined, + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -428,38 +536,52 @@ describe('searchDsl/queryParams', () => { describe('namespace, search, searchFields', () => { it('includes all types for field', () => { - expect(getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title'])).toEqual( - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - createTypeClause('pending', 'foo-namespace'), - createTypeClause('saved', 'foo-namespace'), - createTypeClause('global'), - ], - minimum_should_match: 1, - }, + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'y*', + searchFields: ['title'], + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [ + createTypeClause('pending', 'foo-namespace'), + createTypeClause('saved', 'foo-namespace'), + createTypeClause('global'), + ], + minimum_should_match: 1, }, - ], - must: [ - { - simple_query_string: { - query: 'y*', - fields: ['pending.title', 'saved.title', 'global.title'], - }, + }, + ], + must: [ + { + simple_query_string: { + query: 'y*', + fields: ['pending.title', 'saved.title', 'global.title'], }, - ], - }, + }, + ], }, - } - ); + }, + }); }); it('supports field boosting', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title^3']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'y*', + searchFields: ['title^3'], + }) ).toEqual({ query: { bool: { @@ -489,7 +611,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', undefined, 'y*', ['title', 'title.raw']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: undefined, + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -529,7 +658,14 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), search, searchFields', () => { it('includes all types for field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title'], + }) ).toEqual({ query: { bool: { @@ -555,7 +691,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field boosting', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', ['title^3']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title^3'], + }) ).toEqual({ query: { bool: { @@ -581,10 +724,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, undefined, ['saved', 'global'], 'y*', [ - 'title', - 'title.raw', - ]) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: undefined, + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -613,7 +760,14 @@ describe('searchDsl/queryParams', () => { describe('namespace, type (plural, namespaced and global), search, searchFields', () => { it('includes all types for field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title'], + }) ).toEqual({ query: { bool: { @@ -639,7 +793,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field boosting', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', ['title^3']) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title^3'], + }) ).toEqual({ query: { bool: { @@ -665,10 +826,14 @@ describe('searchDsl/queryParams', () => { }); it('supports field and multi-field', () => { expect( - getQueryParams(MAPPINGS, SCHEMA, 'foo-namespace', ['saved', 'global'], 'y*', [ - 'title', - 'title.raw', - ]) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'y*', + searchFields: ['title', 'title.raw'], + }) ).toEqual({ query: { bool: { @@ -697,15 +862,15 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), search, defaultSearchOperator', () => { it('supports defaultSearchOperator', () => { expect( - getQueryParams( - MAPPINGS, - SCHEMA, - 'foo-namespace', - ['saved', 'global'], - 'foo', - undefined, - 'AND' - ) + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: 'foo', + searchFields: undefined, + defaultSearchOperator: 'AND', + }) ).toEqual({ query: { bool: { @@ -771,19 +936,19 @@ describe('searchDsl/queryParams', () => { describe('type (plural, namespaced and global), hasReference', () => { it('supports hasReference', () => { expect( - getQueryParams( - MAPPINGS, - SCHEMA, - 'foo-namespace', - ['saved', 'global'], - undefined, - undefined, - 'OR', - { + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + type: ['saved', 'global'], + search: undefined, + searchFields: undefined, + defaultSearchOperator: 'OR', + hasReference: { type: 'bar', id: '1', - } - ) + }, + }) ).toEqual({ query: { bool: { @@ -823,4 +988,345 @@ describe('searchDsl/queryParams', () => { }); }); }); + + describe('type filter', () => { + it(' with namespace', () => { + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + kueryNode: { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + }, + indexPattern: INDEX_PATTERN, + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'global.name': 'GLOBAL', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + term: { + type: 'pending', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'saved', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'global', + }, + }, + ], + must_not: [ + { + exists: { + field: 'namespace', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }); + }); + it(' with namespace and more complex filter', () => { + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + kueryNode: { + type: 'function', + function: 'and', + arguments: [ + { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + }, + { + type: 'function', + function: 'not', + arguments: [ + { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'saved.obj.key1' }, + { type: 'literal', value: 'key' }, + { type: 'literal', value: true }, + ], + }, + ], + }, + ], + }, + indexPattern: INDEX_PATTERN, + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'global.name': 'GLOBAL', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match_phrase: { + 'saved.obj.key1': 'key', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + }, + ], + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + term: { + type: 'pending', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'saved', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'global', + }, + }, + ], + must_not: [ + { + exists: { + field: 'namespace', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }); + }); + it(' with search and searchFields', () => { + expect( + getQueryParams({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: 'foo-namespace', + search: 'y*', + searchFields: ['title'], + kueryNode: { + type: 'function', + function: 'is', + arguments: [ + { type: 'literal', value: 'global.name' }, + { type: 'literal', value: 'GLOBAL' }, + { type: 'literal', value: false }, + ], + }, + indexPattern: INDEX_PATTERN, + }) + ).toEqual({ + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'global.name': 'GLOBAL', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + must: [ + { + term: { + type: 'pending', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'saved', + }, + }, + { + term: { + namespace: 'foo-namespace', + }, + }, + ], + }, + }, + { + bool: { + must: [ + { + term: { + type: 'global', + }, + }, + ], + must_not: [ + { + exists: { + field: 'namespace', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + must: [ + { + simple_query_string: { + query: 'y*', + fields: ['pending.title', 'saved.title', 'global.title'], + }, + }, + ], + }, + }, + }); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 9c145258a755ff..125b0c40af9e41 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -16,9 +16,11 @@ * specific language governing permissions and limitations * under the License. */ +import { toElasticsearchQuery, KueryNode } from '@kbn/es-query'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; +import { SavedObjectsIndexPattern } from '../cache_index_patterns'; /** * Gets the types based on the type. Uses mappings to support @@ -76,25 +78,43 @@ function getClauseForType(schema: SavedObjectsSchema, namespace: string | undefi }; } +interface HasReferenceQueryParams { + type: string; + id: string; +} + +interface QueryParams { + mappings: IndexMapping; + schema: SavedObjectsSchema; + namespace?: string; + type?: string | string[]; + search?: string; + searchFields?: string[]; + defaultSearchOperator?: string; + hasReference?: HasReferenceQueryParams; + kueryNode?: KueryNode; + indexPattern?: SavedObjectsIndexPattern; +} + /** * Get the "query" related keys for the search body */ -export function getQueryParams( - mappings: IndexMapping, - schema: SavedObjectsSchema, - namespace?: string, - type?: string | string[], - search?: string, - searchFields?: string[], - defaultSearchOperator?: string, - hasReference?: { - type: string; - id: string; - } -) { +export function getQueryParams({ + mappings, + schema, + namespace, + type, + search, + searchFields, + defaultSearchOperator, + hasReference, + kueryNode, + indexPattern, +}: QueryParams) { const types = getTypes(mappings, type); const bool: any = { filter: [ + ...(kueryNode != null ? [toElasticsearchQuery(kueryNode, indexPattern)] : []), { bool: { must: hasReference diff --git a/src/core/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 index 7bd04ca8f34947..97cab3e566d5ea 100644 --- a/src/core/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 @@ -72,16 +72,16 @@ describe('getSearchDsl', () => { getSearchDsl(MAPPINGS, SCHEMA, opts); expect(getQueryParams).toHaveBeenCalledTimes(1); - expect(getQueryParams).toHaveBeenCalledWith( - MAPPINGS, - SCHEMA, - opts.namespace, - opts.type, - opts.search, - opts.searchFields, - opts.defaultSearchOperator, - opts.hasReference - ); + expect(getQueryParams).toHaveBeenCalledWith({ + mappings: MAPPINGS, + schema: SCHEMA, + namespace: opts.namespace, + type: opts.type, + search: opts.search, + searchFields: opts.searchFields, + defaultSearchOperator: opts.defaultSearchOperator, + hasReference: opts.hasReference, + }); }); it('passes (mappings, type, sortField, sortOrder) to getSortingParams', () => { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 1c2c87bca6ea72..68f60607025053 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -17,12 +17,14 @@ * under the License. */ +import { KueryNode } from '@kbn/es-query'; import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; +import { SavedObjectsIndexPattern } from '../cache_index_patterns'; interface GetSearchDslOptions { type: string | string[]; @@ -36,6 +38,8 @@ interface GetSearchDslOptions { type: string; id: string; }; + kueryNode?: KueryNode; + indexPattern?: SavedObjectsIndexPattern; } export function getSearchDsl( @@ -52,6 +56,8 @@ export function getSearchDsl( sortOrder, namespace, hasReference, + kueryNode, + indexPattern, } = options; if (!type) { @@ -63,7 +69,7 @@ export function getSearchDsl( } return { - ...getQueryParams( + ...getQueryParams({ mappings, schema, namespace, @@ -71,8 +77,10 @@ export function getSearchDsl( search, searchFields, defaultSearchOperator, - hasReference - ), + hasReference, + kueryNode, + indexPattern, + }), ...getSortingParams(mappings, type, sortField, sortOrder), }; } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a7e8f5fd4ac7ca..e7e7a4c64392a6 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -18,6 +18,10 @@ */ import { SavedObjectsClient } from './service/saved_objects_client'; +import { SavedObjectsMapping } from './mappings'; +import { MigrationDefinition } from './migrations/core/document_migrator'; +import { SavedObjectsSchemaDefinition } from './schema'; +import { PropertyValidators } from './validation'; /** * Information about the migrations that have been applied to this SavedObject. @@ -119,6 +123,7 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { searchFields?: string[]; hasReference?: { type: string; id: string }; defaultSearchOperator?: 'AND' | 'OR'; + filter?: string; } /** @@ -201,3 +206,15 @@ export interface SavedObjectsBaseOptions { * @public */ export type SavedObjectsClientContract = Pick; + +/** + * @internal + * @deprecated + */ +export interface SavedObjectsLegacyUiExports { + unknown: [{ pluginSpec: { getId: () => unknown }; type: unknown }] | undefined; + savedObjectMappings: SavedObjectsMapping[]; + savedObjectMigrations: MigrationDefinition; + savedObjectSchemas: SavedObjectsSchemaDefinition; + savedObjectValidations: PropertyValidators; +} diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 94c7f6ec9b3255..6451e2b9b7153e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -10,6 +10,9 @@ import { ConfigOptions } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { IncomingHttpHeaders } from 'http'; +import { IndexPatternsService } from 'src/legacy/server/index_patterns'; +import { KibanaConfigType } from 'src/core/server/kibana_config'; +import { Logger as Logger_2 } from 'src/core/server/logging'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { PeerCertificate } from 'tls'; @@ -18,6 +21,7 @@ import { Request } from 'hapi'; import { ResponseObject } from 'hapi'; import { ResponseToolkit } from 'hapi'; import { Server } from 'hapi'; +import { ShallowPromise } from '@kbn/utility-types'; import { Stream } from 'stream'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; @@ -54,6 +58,17 @@ export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; } +// @public +export class BasePath { + // @internal + constructor(serverBasePath?: string); + get: (request: LegacyRequest | KibanaRequest) => string; + prepend: (path: string) => string; + remove: (path: string) => string; + readonly serverBasePath: string; + set: (request: LegacyRequest | KibanaRequest, requestSpecificBasePath: string) => void; +} + // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts // // @internal (undocumented) @@ -95,7 +110,7 @@ export class ConfigService { // @public export interface ContextSetup { - createContextContainer(): IContextContainer; + createContextContainer>(): IContextContainer; } // @internal (undocumented) @@ -121,7 +136,7 @@ export interface CoreSetup { registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; - registerRouteHandlerContext: (name: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; + registerRouteHandlerContext: (name: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; createRouter: () => IRouter; }; } @@ -205,6 +220,15 @@ export type GetAuthState = (request: KibanaRequest | LegacyRequest) => { state: unknown; }; +// @public +export type HandlerContextType> = T extends HandlerFunction ? U : never; + +// @public +export type HandlerFunction = (context: T, ...args: any[]) => any; + +// @public +export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; + // @public export type Headers = { [header in KnownHeaders]?: string | string[] | undefined; @@ -230,12 +254,7 @@ export interface HttpServerSetup { getAuthHeaders: GetAuthHeaders; }; // (undocumented) - basePath: { - get: (request: KibanaRequest | LegacyRequest) => string; - set: (request: KibanaRequest | LegacyRequest, basePath: string) => void; - prepend: (url: string) => string; - remove: (url: string) => string; - }; + basePath: IBasePath; createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; isTlsEnabled: boolean; registerAuth: (handler: AuthenticationHandler) => void; @@ -249,7 +268,7 @@ export interface HttpServerSetup { // @public (undocumented) export type HttpServiceSetup = Omit & { createRouter: (path: string, plugin?: PluginOpaqueId) => IRouter; - registerRouteHandlerContext: (pluginOpaqueId: PluginOpaqueId, contextName: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; + registerRouteHandlerContext: (pluginOpaqueId: PluginOpaqueId, contextName: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; }; // @public (undocumented) @@ -258,16 +277,16 @@ export interface HttpServiceStart { } // @public -export interface IContextContainer { - createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise; - registerContext(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; -} +export type IBasePath = Pick; // @public -export type IContextHandler = (context: TContext, ...rest: THandlerParameters) => TReturn; +export interface IContextContainer> { + createHandler(pluginOpaqueId: PluginOpaqueId, handler: THandler): (...rest: HandlerParameters) => ShallowPromise>; + registerContext>(pluginOpaqueId: PluginOpaqueId, contextName: TContextName, provider: IContextProvider): this; +} // @public -export type IContextProvider, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; +export type IContextProvider, TContextName extends keyof HandlerContextType> = (context: Partial>, ...rest: HandlerParameters) => Promise[TContextName]> | HandlerContextType[TContextName]; // @public export interface IKibanaSocket { @@ -290,8 +309,12 @@ export interface InternalCoreSetup { http: HttpServiceSetup; } -// @public (undocumented) +// @internal (undocumented) export interface InternalCoreStart { + // Warning: (ae-forgotten-export) The symbol "SavedObjectsServiceStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + savedObjects: SavedObjectsServiceStart; } // @public @@ -387,6 +410,8 @@ export interface LegacyServiceSetupDeps { // @public @deprecated (undocumented) export interface LegacyServiceStartDeps { + // Warning: (ae-incompatible-release-tags) The symbol "core" is marked as @public, but its signature references "InternalCoreStart" which is marked as @internal + // // (undocumented) core: InternalCoreStart & { plugins: PluginsServiceStart; @@ -584,16 +609,10 @@ export interface RequestHandlerContext { } // @public -export type RequestHandlerContextContainer = IContextContainer, RequestHandlerParams>; - -// @public -export type RequestHandlerContextProvider = IContextProvider; +export type RequestHandlerContextContainer = IContextContainer>; // @public -export type RequestHandlerParams = [KibanaRequest, KibanaResponseFactory]; - -// @public -export type RequestHandlerReturn = KibanaResponse; +export type RequestHandlerContextProvider = IContextProvider, TContextName>; // @public export type ResponseError = string | Error | { @@ -824,6 +843,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // (undocumented) + filter?: string; + // (undocumented) hasReference?: { type: string; id: string; @@ -946,6 +967,31 @@ export interface SavedObjectsImportUnsupportedTypeError { type: 'unsupported_type'; } +// @internal @deprecated (undocumented) +export interface SavedObjectsLegacyService { + // 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']; + // (undocumented) + importExport: { + objectLimit: number; + importSavedObjects(options: SavedObjectsImportOptions): Promise; + resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; + getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; + }; + // (undocumented) + SavedObjectsClient: typeof SavedObjectsClient; + // (undocumented) + schema: SavedObjectsSchema; + // (undocumented) + types: string[]; +} + // @public (undocumented) export interface SavedObjectsMigrationLogger { // (undocumented) @@ -994,9 +1040,7 @@ export interface SavedObjectsResolveImportErrorsOptions { supportedTypes: string[]; } -// Warning: (ae-missing-release-tag) "SavedObjectsSchema" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @internal (undocumented) export class SavedObjectsSchema { // Warning: (ae-forgotten-export) The symbol "SavedObjectsSchemaDefinition" needs to be exported by the entry point index.d.ts constructor(schemaDefinition?: SavedObjectsSchemaDefinition); @@ -1010,9 +1054,7 @@ export class SavedObjectsSchema { isNamespaceAgnostic(type: string): boolean; } -// Warning: (ae-missing-release-tag) "SavedObjectsSerializer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @internal (undocumented) export class SavedObjectsSerializer { constructor(schema: SavedObjectsSchema); generateRawId(namespace: string | undefined, type: string, id?: string): string; @@ -1022,33 +1064,6 @@ export class SavedObjectsSerializer { savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): SavedObjectsRawDoc; } -// @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']; - // (undocumented) - importExport: { - objectLimit: number; - importSavedObjects(options: SavedObjectsImportOptions): Promise; - resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; - getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; - }; - // 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) - schema: SavedObjectsSchema; - // (undocumented) - types: string[]; -} - // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { // (undocumented) diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 694888ab6243eb..cb1a88f6e8aed8 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -23,6 +23,7 @@ import { mockLegacyService, mockPluginsService, mockConfigService, + mockSavedObjectsService, } from './index.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -51,6 +52,7 @@ test('sets up services on "setup"', async () => { expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); + expect(mockSavedObjectsService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -58,6 +60,7 @@ test('sets up services on "setup"', async () => { expect(mockElasticsearchService.setup).toHaveBeenCalledTimes(1); expect(mockPluginsService.setup).toHaveBeenCalledTimes(1); expect(mockLegacyService.setup).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); }); test('runs services on "start"', async () => { @@ -70,10 +73,12 @@ test('runs services on "start"', async () => { expect(mockHttpService.start).not.toHaveBeenCalled(); expect(mockLegacyService.start).not.toHaveBeenCalled(); + expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); await server.start(); expect(mockHttpService.start).toHaveBeenCalledTimes(1); expect(mockLegacyService.start).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -93,6 +98,7 @@ test('stops services on "stop"', async () => { expect(mockElasticsearchService.stop).not.toHaveBeenCalled(); expect(mockPluginsService.stop).not.toHaveBeenCalled(); expect(mockLegacyService.stop).not.toHaveBeenCalled(); + expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -100,6 +106,7 @@ test('stops services on "stop"', async () => { expect(mockElasticsearchService.stop).toHaveBeenCalledTimes(1); expect(mockPluginsService.stop).toHaveBeenCalledTimes(1); expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); + expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e0569ed80fca4a..2b63d6ac3be1c9 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -26,11 +26,14 @@ import { HttpService, HttpServiceSetup } from './http'; import { LegacyService } from './legacy'; import { Logger, LoggerFactory } from './logging'; import { PluginsService, config as pluginsConfig } from './plugins'; +import { SavedObjectsService } from '../server/saved_objects'; import { config as elasticsearchConfig } from './elasticsearch'; import { config as httpConfig } from './http'; import { config as loggingConfig } from './logging'; import { config as devConfig } from './dev'; +import { config as kibanaConfig } from './kibana_config'; +import { config as savedObjectsConfig } from './saved_objects'; import { mapToObject } from '../utils/'; import { ContextService } from './context'; import { InternalCoreSetup } from './index'; @@ -42,9 +45,10 @@ export class Server { private readonly context: ContextService; private readonly elasticsearch: ElasticsearchService; private readonly http: HttpService; - private readonly plugins: PluginsService; private readonly legacy: LegacyService; private readonly log: Logger; + private readonly plugins: PluginsService; + private readonly savedObjects: SavedObjectsService; constructor( readonly config$: Observable, @@ -60,6 +64,7 @@ export class Server { this.plugins = new PluginsService(core); this.legacy = new LegacyService(core); this.elasticsearch = new ElasticsearchService(core); + this.savedObjects = new SavedObjectsService(core); } public async setup() { @@ -88,18 +93,26 @@ export class Server { this.registerCoreContext(coreSetup); const pluginsSetup = await this.plugins.setup(coreSetup); - await this.legacy.setup({ + const legacySetup = await this.legacy.setup({ core: { ...coreSetup, plugins: pluginsSetup }, plugins: mapToObject(pluginsSetup.contracts), }); + await this.savedObjects.setup({ + elasticsearch: elasticsearchServiceSetup, + legacy: legacySetup, + }); + return coreSetup; } public async start() { + this.log.debug('starting server'); const pluginsStart = await this.plugins.start({}); + const savedObjectsStart = await this.savedObjects.start({}); const coreStart = { + savedObjects: savedObjectsStart, plugins: pluginsStart, }; @@ -109,6 +122,7 @@ export class Server { }); await this.http.start(); + return coreStart; } @@ -117,6 +131,7 @@ export class Server { await this.legacy.stop(); await this.plugins.stop(); + await this.savedObjects.stop(); await this.elasticsearch.stop(); await this.http.stop(); } @@ -148,6 +163,8 @@ export class Server { [httpConfig.path, httpConfig.schema], [pluginsConfig.path, pluginsConfig.schema], [devConfig.path, devConfig.schema], + [kibanaConfig.path, kibanaConfig.schema], + [savedObjectsConfig.path, savedObjectsConfig.schema], ]; for (const [path, schema] of schemas) { diff --git a/src/core/utils/context.mock.ts b/src/core/utils/context.mock.ts index 4d91c11542b2f8..de844f3f0f07db 100644 --- a/src/core/utils/context.mock.ts +++ b/src/core/utils/context.mock.ts @@ -19,7 +19,7 @@ import { IContextContainer } from './context'; -export type ContextContainerMock = jest.Mocked>; +export type ContextContainerMock = jest.Mocked>; const createContextMock = () => { const contextMock: ContextContainerMock = { diff --git a/src/core/utils/context.test.ts b/src/core/utils/context.test.ts index 1249c14736fb53..4bfeddc2e08c97 100644 --- a/src/core/utils/context.test.ts +++ b/src/core/utils/context.test.ts @@ -44,7 +44,7 @@ const coreId = Symbol(); describe('ContextContainer', () => { it('does not allow the same context to be registered twice', () => { - const contextContainer = new ContextContainer(plugins, coreId); + const contextContainer = new ContextContainer<(context: MyContext) => string>(plugins, coreId); contextContainer.registerContext(coreId, 'ctxFromA', () => 'aString'); expect(() => @@ -56,7 +56,10 @@ describe('ContextContainer', () => { describe('registerContext', () => { it('throws error if called with an unknown symbol', async () => { - const contextContainer = new ContextContainer(plugins, coreId); + const contextContainer = new ContextContainer<(context: MyContext) => string>( + plugins, + coreId + ); await expect(() => contextContainer.registerContext(Symbol('unknown'), 'ctxFromA', jest.fn()) ).toThrowErrorMatchingInlineSnapshot( @@ -67,7 +70,10 @@ describe('ContextContainer', () => { describe('context building', () => { it('resolves dependencies', async () => { - const contextContainer = new ContextContainer(plugins, coreId); + const contextContainer = new ContextContainer<(context: MyContext) => string>( + plugins, + coreId + ); expect.assertions(8); contextContainer.registerContext(coreId, 'core1', context => { expect(context).toEqual({}); @@ -118,7 +124,10 @@ describe('ContextContainer', () => { it('exposes all core context to all providers regardless of registration order', async () => { expect.assertions(4); - const contextContainer = new ContextContainer(plugins, coreId); + const contextContainer = new ContextContainer<(context: MyContext) => string>( + plugins, + coreId + ); contextContainer .registerContext(pluginA, 'ctxFromA', context => { expect(context).toEqual({ core1: 'core', core2: 101 }); @@ -146,7 +155,10 @@ describe('ContextContainer', () => { it('exposes all core context to core providers', async () => { expect.assertions(4); - const contextContainer = new ContextContainer(plugins, coreId); + const contextContainer = new ContextContainer<(context: MyContext) => string>( + plugins, + coreId + ); contextContainer .registerContext(coreId, 'core1', context => { @@ -171,7 +183,10 @@ describe('ContextContainer', () => { }); it('does not expose plugin contexts to core handler', async () => { - const contextContainer = new ContextContainer(plugins, coreId); + const contextContainer = new ContextContainer<(context: MyContext) => string>( + plugins, + coreId + ); contextContainer .registerContext(coreId, 'core1', context => 'core') @@ -189,10 +204,9 @@ describe('ContextContainer', () => { it('passes additional arguments to providers', async () => { expect.assertions(6); - const contextContainer = new ContextContainer( - plugins, - coreId - ); + const contextContainer = new ContextContainer< + (context: MyContext, arg1: string, arg2: number) => string + >(plugins, coreId); contextContainer.registerContext(coreId, 'core1', (context, str, num) => { expect(str).toEqual('passed string'); @@ -228,7 +242,10 @@ describe('ContextContainer', () => { describe('createHandler', () => { it('throws error if called with an unknown symbol', async () => { - const contextContainer = new ContextContainer(plugins, coreId); + const contextContainer = new ContextContainer<(context: MyContext) => string>( + plugins, + coreId + ); await expect(() => contextContainer.createHandler(Symbol('unknown'), jest.fn()) ).toThrowErrorMatchingInlineSnapshot( @@ -237,8 +254,10 @@ describe('ContextContainer', () => { }); it('returns value from original handler', async () => { - const contextContainer = new ContextContainer(plugins, coreId); - + const contextContainer = new ContextContainer<(context: MyContext) => string>( + plugins, + coreId + ); const rawHandler1 = jest.fn(() => 'handler1'); const handler1 = contextContainer.createHandler(pluginA, rawHandler1); @@ -246,10 +265,9 @@ describe('ContextContainer', () => { }); it('passes additional arguments to handlers', async () => { - const contextContainer = new ContextContainer( - plugins, - coreId - ); + const contextContainer = new ContextContainer< + (context: MyContext, arg1: string, arg2: number) => string + >(plugins, coreId); const rawHandler1 = jest.fn(() => 'handler1'); const handler1 = contextContainer.createHandler(pluginA, rawHandler1); diff --git a/src/core/utils/context.ts b/src/core/utils/context.ts index 6d1732ea06b0e1..022c3e43300322 100644 --- a/src/core/utils/context.ts +++ b/src/core/utils/context.ts @@ -18,6 +18,7 @@ */ import { flatten } from 'lodash'; +import { ShallowPromise } from '@kbn/utility-types'; import { pick } from '.'; import { CoreId, PluginOpaqueId } from '../server'; @@ -35,26 +36,44 @@ import { CoreId, PluginOpaqueId } from '../server'; * @public */ export type IContextProvider< - TContext extends Record, - TContextName extends keyof TContext, - TProviderParameters extends any[] = [] + THandler extends HandlerFunction, + TContextName extends keyof HandlerContextType > = ( - context: Partial, - ...rest: TProviderParameters -) => Promise | TContext[TContextName]; + context: Partial>, + ...rest: HandlerParameters +) => + | Promise[TContextName]> + | HandlerContextType[TContextName]; /** - * A function registered by a plugin to perform some action. + * A function that accepts a context object and an optional number of additional arguments. Used for the generic types + * in {@link IContextContainer} * - * @remarks - * A new `TContext` will be built for each handler before invoking. + * @public + */ +export type HandlerFunction = (context: T, ...args: any[]) => any; + +/** + * Extracts the type of the first argument of a {@link HandlerFunction} to represent the type of the context. * * @public */ -export type IContextHandler = ( - context: TContext, - ...rest: THandlerParameters -) => TReturn; +export type HandlerContextType> = T extends HandlerFunction + ? U + : never; + +/** + * Extracts the types of the additional arguments of a {@link HandlerFunction}, excluding the + * {@link HandlerContextType}. + * + * @public + */ +export type HandlerParameters> = T extends ( + context: any, + ...args: infer U +) => any + ? U + : never; /** * An object that handles registration of context providers and configuring handlers with context. @@ -123,13 +142,12 @@ export type IContextHandler { +export interface IContextContainer> { /** * Register a new context provider. * @@ -144,10 +162,10 @@ export interface IContextContainer< * @param provider - A {@link IContextProvider} to be called each time a new context is created. * @returns The {@link IContextContainer} for method chaining. */ - registerContext( + registerContext>( pluginOpaqueId: PluginOpaqueId, contextName: TContextName, - provider: IContextProvider + provider: IContextProvider ): this; /** @@ -160,31 +178,26 @@ export interface IContextContainer< */ createHandler( pluginOpaqueId: PluginOpaqueId, - handler: IContextHandler - ): ( - ...rest: THandlerParameters - ) => THandlerReturn extends Promise ? THandlerReturn : Promise; + handler: THandler + ): (...rest: HandlerParameters) => ShallowPromise>; } /** @internal */ -export class ContextContainer< - TContext extends Record, - THandlerReturn, - THandlerParameters extends any[] = [] -> implements IContextContainer { +export class ContextContainer> + implements IContextContainer { /** * Used to map contexts to their providers and associated plugin. In registration order which is tightly coupled to * plugin load order. */ private readonly contextProviders = new Map< - keyof TContext, + keyof HandlerContextType, { - provider: IContextProvider; + provider: IContextProvider>; source: symbol; } >(); /** Used to keep track of which plugins registered which contexts for dependency resolution. */ - private readonly contextNamesBySource: Map>; + private readonly contextNamesBySource: Map>>; /** * @param pluginDependencies - A map of plugins to an array of their dependencies. @@ -193,13 +206,15 @@ export class ContextContainer< private readonly pluginDependencies: ReadonlyMap, private readonly coreId: CoreId ) { - this.contextNamesBySource = new Map>([[coreId, []]]); + this.contextNamesBySource = new Map>>([ + [coreId, []], + ]); } - public registerContext = ( + public registerContext = >( source: symbol, contextName: TContextName, - provider: IContextProvider + provider: IContextProvider ): this => { if (this.contextProviders.has(contextName)) { throw new Error(`Context provider for ${contextName} has already been registered.`); @@ -217,27 +232,22 @@ export class ContextContainer< return this; }; - public createHandler = ( - source: symbol, - handler: IContextHandler - ) => { + public createHandler = (source: symbol, handler: THandler) => { if (source !== this.coreId && !this.pluginDependencies.has(source)) { throw new Error(`Cannot create handler for unknown plugin: ${source.toString()}`); } - return (async (...args: THandlerParameters) => { + return (async (...args: HandlerParameters) => { const context = await this.buildContext(source, ...args); return handler(context, ...args); - }) as ( - ...args: THandlerParameters - ) => THandlerReturn extends Promise ? THandlerReturn : Promise; + }) as (...args: HandlerParameters) => ShallowPromise>; }; private async buildContext( source: symbol, - ...contextArgs: THandlerParameters - ): Promise { - const contextsToBuild: ReadonlySet = new Set( + ...contextArgs: HandlerParameters + ): Promise> { + const contextsToBuild: ReadonlySet> = new Set( this.getContextNamesForSource(source) ); @@ -252,18 +262,20 @@ export class ContextContainer< // registered that provider. const exposedContext = pick(resolvedContext, [ ...this.getContextNamesForSource(providerSource), - ]); + ]) as Partial>; return { ...resolvedContext, - [contextName]: await provider(exposedContext as Partial, ...contextArgs), + [contextName]: await provider(exposedContext, ...contextArgs), }; }, - Promise.resolve({}) as Promise + Promise.resolve({}) as Promise> ); } - private getContextNamesForSource(source: symbol): ReadonlySet { + private getContextNamesForSource( + source: symbol + ): ReadonlySet> { if (source === this.coreId) { return this.getContextNamesForCore(); } else { diff --git a/src/dev/register_git_hook/register_git_hook.js b/src/dev/register_git_hook/register_git_hook.js index bdb3e2d8060302..a61922078e6876 100644 --- a/src/dev/register_git_hook/register_git_hook.js +++ b/src/dev/register_git_hook/register_git_hook.js @@ -37,7 +37,7 @@ const writeFileAsync = promisify(writeFile); async function getPrecommitGitHookScriptPath(rootPath) { // Retrieves the correct location for the .git dir for // every git setup (including git worktree) - const gitDirPath = (await gitRevParseAsync(['--git-dir'])).trim(); + const gitDirPath = (await gitRevParseAsync(['--git-common-dir'])).trim(); return resolve(rootPath, gitDirPath, 'hooks/pre-commit'); } diff --git a/src/dev/renovate/config.ts b/src/dev/renovate/config.ts index a86c080f527102..1bf3af9fe5248e 100644 --- a/src/dev/renovate/config.ts +++ b/src/dev/renovate/config.ts @@ -86,6 +86,7 @@ export const RENOVATE_CONFIG = { labels: group.extraLabels && [...DEFAULT_LABELS, ...group.extraLabels], enabled: group.enabled === false ? false : undefined, allowedVersions: group.allowedVersions || undefined, + reviewers: group.reviewers || undefined, })), // internal/local packages diff --git a/src/dev/renovate/package_groups.ts b/src/dev/renovate/package_groups.ts index 85692e94eb9127..185163890f7458 100644 --- a/src/dev/renovate/package_groups.ts +++ b/src/dev/renovate/package_groups.ts @@ -56,6 +56,11 @@ interface PackageGroup { * https://renovatebot.com/docs/configuration-options/#allowedversions */ readonly allowedVersions?: string; + + /** + * An array of users to request reviews from + */ + readonly reviewers?: string[]; } export const RENOVATE_PACKAGE_GROUPS: PackageGroup[] = [ @@ -75,6 +80,12 @@ export const RENOVATE_PACKAGE_GROUPS: PackageGroup[] = [ packageWords: ['jest'], }, + { + name: '@elastic/charts', + packageNames: ['@elastic/charts'], + reviewers: ['markov00'], + }, + { name: 'mocha', packageWords: ['mocha'], diff --git a/src/dev/run_check_core_api_changes.ts b/src/dev/run_check_core_api_changes.ts index 4d0be7f3884665..d2c75c86ce7442 100644 --- a/src/dev/run_check_core_api_changes.ts +++ b/src/dev/run_check_core_api_changes.ts @@ -139,14 +139,51 @@ const runApiExtractor = ( return Extractor.invoke(config, options); }; -async function run(folder: string): Promise { +interface Options { + accept: boolean; + docs: boolean; + help: boolean; +} + +async function run( + folder: string, + { log, opts }: { log: ToolingLog; opts: Options } +): Promise { + log.info(`Core ${folder} API: checking for changes in API signature...`); + + const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); + + // If we're not accepting changes and there's a failure, exit. + if (!opts.accept && !succeeded) { + return false; + } + + // Attempt to generate docs even if api-extractor didn't succeed + if ((opts.accept && apiReportChanged) || opts.docs) { + try { + await renameExtractedApiPackageName(folder); + await runApiDocumenter(folder); + } catch (e) { + log.error(e); + return false; + } + log.info(`Core ${folder} API: updated documentation ✔`); + } + + // If the api signature changed or any errors or warnings occured, exit with an error + // NOTE: Because of https://github.com/Microsoft/web-build-tools/issues/1258 + // api-extractor will not return `succeeded: false` when the API changes. + return !apiReportChanged && succeeded; +} + +(async () => { const log = new ToolingLog({ level: 'info', writeTo: process.stdout, }); const extraFlags: string[] = []; - const opts = getopts(process.argv.slice(2), { + const opts = (getopts(process.argv.slice(2), { boolean: ['accept', 'docs', 'help'], default: { project: undefined, @@ -155,7 +192,7 @@ async function run(folder: string): Promise { extraFlags.push(name); return false; }, - }); + }) as any) as Options; if (extraFlags.length > 0) { for (const flag of extraFlags) { @@ -193,45 +230,18 @@ async function run(folder: string): Promise { return !(extraFlags.length > 0); } - log.info(`Core ${folder} API: checking for changes in API signature...`); - try { + log.info(`Core: Building types...`); await runBuildTypes(); } catch (e) { log.error(e); return false; } - const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); - - // If we're not accepting changes and there's a failure, exit. - if (!opts.accept && !succeeded) { - return false; - } - - // Attempt to generate docs even if api-extractor didn't succeed - if ((opts.accept && apiReportChanged) || opts.docs) { - try { - await renameExtractedApiPackageName(folder); - await runApiDocumenter(folder); - } catch (e) { - log.error(e); - return false; - } - log.info(`Core ${folder} API: updated documentation ✔`); - } - - // If the api signature changed or any errors or warnings occured, exit with an error - // NOTE: Because of https://github.com/Microsoft/web-build-tools/issues/1258 - // api-extractor will not return `succeeded: false` when the API changes. - return !apiReportChanged && succeeded; -} - -(async () => { - const publicSucceeded = await run('public'); - const serverSucceeded = await run('server'); + const folders = ['public', 'server']; + const results = await Promise.all(folders.map(folder => run(folder, { log, opts }))); - if (!publicSucceeded || !serverSucceeded) { + if (results.find(r => r === false) !== undefined) { process.exitCode = 1; } })(); diff --git a/src/es_archiver/actions/empty_kibana_index.js b/src/es_archiver/actions/empty_kibana_index.js index 411a8b18038a78..adcf65711a4889 100644 --- a/src/es_archiver/actions/empty_kibana_index.js +++ b/src/es_archiver/actions/empty_kibana_index.js @@ -20,12 +20,11 @@ import { migrateKibanaIndex, deleteKibanaIndices, createStats, - getEnabledKibanaPluginIds } from '../lib'; -export async function emptyKibanaIndexAction({ client, log, kibanaUrl }) { +export async function emptyKibanaIndexAction({ client, log, kbnClient }) { const stats = createStats('emptyKibanaIndex', log); - const kibanaPluginIds = await getEnabledKibanaPluginIds(kibanaUrl); + const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); await deleteKibanaIndices({ client, stats }); await migrateKibanaIndex({ client, log, stats, kibanaPluginIds }); diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.js index b68bc38096d745..c54fe8c99b7253 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.js @@ -35,7 +35,6 @@ import { createIndexDocRecordsStream, migrateKibanaIndex, Progress, - getEnabledKibanaPluginIds, createDefaultSpace, } from '../lib'; @@ -49,11 +48,11 @@ const pipeline = (...streams) => streams .pipe(dest) )); -export async function loadAction({ name, skipExisting, client, dataDir, log, kibanaUrl }) { +export async function loadAction({ name, skipExisting, client, dataDir, log, kbnClient }) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const files = prioritizeMappings(await readDirectory(inputDir)); - const kibanaPluginIds = await getEnabledKibanaPluginIds(kibanaUrl); + const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); // a single stream that emits records from all archive files, in // order, so that createIndexStream can track the state of indexes diff --git a/src/es_archiver/actions/unload.js b/src/es_archiver/actions/unload.js index b6aead8c25f21f..c8ed868f9ff172 100644 --- a/src/es_archiver/actions/unload.js +++ b/src/es_archiver/actions/unload.js @@ -32,13 +32,12 @@ import { createParseArchiveStreams, createFilterRecordsStream, createDeleteIndexStream, - getEnabledKibanaPluginIds, } from '../lib'; -export async function unloadAction({ name, client, dataDir, log, kibanaUrl }) { +export async function unloadAction({ name, client, dataDir, log, kbnClient }) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); - const kibanaPluginIds = await getEnabledKibanaPluginIds(kibanaUrl); + const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); const files = prioritizeMappings(await readDirectory(inputDir)); for (const filename of files) { diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.js index 9ffff2b28d127f..c4871ad7867915 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.js @@ -17,6 +17,8 @@ * under the License. */ +import { KbnClient } from '@kbn/dev-utils'; + import { saveAction, loadAction, @@ -31,7 +33,7 @@ export class EsArchiver { this.client = client; this.dataDir = dataDir; this.log = log; - this.kibanaUrl = kibanaUrl; + this.kbnClient = new KbnClient(log, [kibanaUrl]); } /** @@ -73,7 +75,7 @@ export class EsArchiver { client: this.client, dataDir: this.dataDir, log: this.log, - kibanaUrl: this.kibanaUrl, + kbnClient: this.kbnClient, }); } @@ -89,7 +91,7 @@ export class EsArchiver { client: this.client, dataDir: this.dataDir, log: this.log, - kibanaUrl: this.kibanaUrl, + kbnClient: this.kbnClient, }); } @@ -144,7 +146,7 @@ export class EsArchiver { await emptyKibanaIndexAction({ client: this.client, log: this.log, - kibanaUrl: this.kibanaUrl, + kbnClient: this.kbnClient, }); } } diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.js index 9a21201152b966..8632a493dd5341 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.js @@ -53,7 +53,3 @@ export { export { Progress } from './progress'; - -export { - getEnabledKibanaPluginIds, -} from './kibana_plugins'; diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index b751ca2f1864b0..dc916e11d698c0 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -77,39 +77,41 @@ export async function deleteKibanaIndices({ client, stats, log }) { */ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { const uiExports = await getUiExports(kibanaPluginIds); - const version = await loadElasticVersion(); + const kibanaVersion = await loadKibanaVersion(); + const config = { - 'kibana.index': '.kibana', - 'migrations.scrollDuration': '5m', - 'migrations.batchSize': 100, - 'migrations.pollInterval': 100, 'xpack.task_manager.index': '.kibana_task_manager', }; - const ready = async () => undefined; - const elasticsearch = { - getCluster: () => ({ - callWithInternalUser: (path, ...args) => _.get(client, path).call(client, ...args), - }), - waitUntilReady: ready, - }; - - const server = { - log: ([logType, messageType], ...args) => log[logType](`[${messageType}] ${args.join(' ')}`), - config: () => ({ get: path => config[path] }), - plugins: { elasticsearch }, - }; - const kbnServer = { - server, - version, - uiExports, - ready, + const migratorOptions = { + config: { get: path => config[path] }, + savedObjectsConfig: { + 'scrollDuration': '5m', + 'batchSize': 100, + 'pollInterval': 100, + }, + kibanaConfig: { + index: '.kibana', + }, + logger: { + trace: log.verbose.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + error: log.error.bind(log), + }, + version: kibanaVersion, + savedObjectSchemas: uiExports.savedObjectSchemas, + savedObjectMappings: uiExports.savedObjectMappings, + savedObjectMigrations: uiExports.savedObjectMigrations, + savedObjectValidations: uiExports.savedObjectValidations, + callCluster: (path, ...args) => _.get(client, path).call(client, ...args), }; - return await new KibanaMigrator({ kbnServer }).awaitMigration(); + return await new KibanaMigrator(migratorOptions).runMigrations(); } -async function loadElasticVersion() { +async function loadKibanaVersion() { const readFile = promisify(fs.readFile); const packageJson = await readFile(path.join(__dirname, '../../../../package.json')); return JSON.parse(packageJson).version; diff --git a/src/es_archiver/lib/kibana_plugins.ts b/src/es_archiver/lib/kibana_plugins.ts deleted file mode 100644 index 44552d5ab20396..00000000000000 --- a/src/es_archiver/lib/kibana_plugins.ts +++ /dev/null @@ -1,55 +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 Axios from 'axios'; - -const PLUGIN_STATUS_ID = /^plugin:(.+?)@/; -const isString = (v: any): v is string => typeof v === 'string'; - -/** - * Get the list of enabled plugins from Kibana, used to determine which - * uiExports to collect, whether we should clean or clean the kibana index, - * and if we need to inject the default space document in new versions of - * the index. - * - * This must be called before touching the Kibana index as Kibana becomes - * unstable when the .kibana index is deleted/cleaned and the status API - * will fail in situations where status.allowAnonymous=false and security - * is enabled. - */ -export async function getEnabledKibanaPluginIds(kibanaUrl: string): Promise { - try { - const { data } = await Axios.get('/api/status', { - baseURL: kibanaUrl, - }); - - return (data.status.statuses as Array<{ id: string }>) - .map(({ id }) => { - const match = id.match(PLUGIN_STATUS_ID); - if (match) { - return match[1]; - } - }) - .filter(isString); - } catch (error) { - throw new Error( - `Unable to fetch Kibana status API response from Kibana at ${kibanaUrl}: ${error}` - ); - } -} diff --git a/src/legacy/core_plugins/console/np_ready/public/application/components/console_menu.tsx b/src/legacy/core_plugins/console/np_ready/public/application/components/console_menu.tsx index 88168d6278f1ec..643b29578674cc 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/components/console_menu.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/components/console_menu.tsx @@ -106,6 +106,7 @@ export class ConsoleMenu extends Component { { , { this.openDocs(); }} @@ -142,7 +144,11 @@ export class ConsoleMenu extends Component { defaultMessage="Open documentation" /> , - + {}); +jest.mock('../../../../../../../public/quarantined/src/mappings.js', () => ({ + retrieveAutoCompleteInfo: () => {}, +})); +jest.mock('../../../../../../../public/quarantined/src/input.js', () => { + return { + initializeInput: () => ({ + $el: { + css: () => {}, + }, + focus: () => {}, + update: () => {}, + getSession: () => ({ on: () => {}, setUseWrapMode: () => {} }), + commands: { + addCommand: () => {}, + }, + }), + }; +}); + +import * as sendRequestModule from './send_current_request_to_es'; +import * as consoleMenuActions from '../console_menu_actions'; + +describe('Legacy (Ace) Console Editor Component Smoke Test', () => { + let mockedAppContextValue: any; + let editor: ReactWrapper; + + beforeEach(() => { + document.queryCommandSupported = sinon.fake(() => true); + mockedAppContextValue = { + services: { + history: { + getSavedEditorState: () => null, + updateCurrentState: () => {}, + }, + }, + // eslint-disable-next-line + ResizeChecker: function() { + return { on: () => {} }; + }, + docLinkVersion: 'NA', + }; + editor = mount( + + + + + + + + ); + }); + + it('calls send current request to ES', () => { + const stub = sinon.stub(sendRequestModule, 'sendCurrentRequestToES'); + try { + editor.find('[data-test-subj~="sendRequestButton"]').simulate('click'); + expect(stub.called).toBe(true); + expect(stub.callCount).toBe(1); + } finally { + stub.restore(); + } + }); + + it('opens docs', () => { + const stub = sinon.stub(consoleMenuActions, 'getDocumentation'); + try { + const consoleMenuToggle = editor.find('[data-test-subj~="toggleConsoleMenu"]').last(); + consoleMenuToggle.simulate('click'); + + const docsButton = editor.find('[data-test-subj~="consoleMenuOpenDocs"]').last(); + docsButton.simulate('click'); + + expect(stub.called).toBe(true); + expect(stub.callCount).toBe(1); + } finally { + stub.restore(); + } + }); + + it('prompts auto-indent', () => { + const stub = sinon.stub(consoleMenuActions, 'autoIndent'); + try { + const consoleMenuToggle = editor.find('[data-test-subj~="toggleConsoleMenu"]').last(); + consoleMenuToggle.simulate('click'); + + const autoIndentButton = editor.find('[data-test-subj~="consoleMenuAutoIndent"]').last(); + autoIndentButton.simulate('click'); + + expect(stub.called).toBe(true); + expect(stub.callCount).toBe(1); + } finally { + stub.restore(); + } + }); +}); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx index 005cf14c32a1d5..df0e1af0204b3c 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -181,7 +181,7 @@ function _Editor({ previousStateLocation = 'stored' }: EditorProps) { > - - - - - - - - - - - - - - - +
+ + + +
+ + + + + + + + + + + + + + + + + + + `; exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` - + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-expanded={false} + aria-haspopup="true" + aria-label="Search and filter the test page" + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + role="combobox" + style={ + Object { + "position": "relative", } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} + } + > +
- -
+ } + aria-autocomplete="list" + aria-label="Start typing to search and filter the test page" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" > -
+ } + compressed={false} + fullWidth={true} + isLoading={false} > - -
+ - + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} > - - - Lucene - - - - - -
-
- -
-
-
-
-
-
-
- -
-
-
+
+ + + +
+ + + + + + + + + + + + + + + + + + + `; exports[`QueryBarInput Should render the given query 1`] = ` - + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-expanded={false} + aria-haspopup="true" + aria-label="Search and filter the test page" + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + role="combobox" + style={ + Object { + "position": "relative", } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="popover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - withTitle={true} + } + > +
- -
+ } + aria-autocomplete="list" + aria-label="Start typing to search and filter the test page" + autoComplete="off" + autoFocus={true} + compressed={false} + data-test-subj="queryInput" + fullWidth={true} + inputRef={[Function]} + isLoading={false} + onChange={[Function]} + onClick={[Function]} onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + onKeyUp={[Function]} + placeholder="Search" + role="textbox" + spellCheck={false} + type="text" + value="response:200" > -
+ } + compressed={false} + fullWidth={true} + isLoading={false} > - -
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="popover" + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + withTitle={true} > - - - KQL - - - - - -
-
- -
-
-
-
-
-
-
- -
-
-
+
+ + + +
+ + + + + + + + + + + + + + + + + + + `; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx index e66d71b9b08b4b..a66fb682063ece 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.test.tsx @@ -25,12 +25,14 @@ import { import { EuiFieldText } from '@elastic/eui'; import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { QueryLanguageSwitcher } from './language_switcher'; import { QueryBarInput, QueryBarInputUI } from './query_bar_input'; import { coreMock } from '../../../../../../../core/public/mocks'; const startMock = coreMock.createStart(); import { IndexPattern } from '../../../index'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { mount } from 'enzyme'; const noop = () => { return; @@ -78,64 +80,67 @@ const mockIndexPattern = { ], } as IndexPattern; +function wrapQueryBarInputInContext(testProps: any, store?: any) { + const defaultOptions = { + screenTitle: 'Another Screen', + intl: null as any, + }; + + const services = { + appName: testProps.appName || 'test', + uiSettings: startMock.uiSettings, + savedObjects: startMock.savedObjects, + notifications: startMock.notifications, + http: startMock.http, + store: store || createMockStorage(), + }; + + return ( + + + + + + ); +} + describe('QueryBarInput', () => { beforeEach(() => { jest.clearAllMocks(); }); it('Should render the given query', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + }) ); expect(component).toMatchSnapshot(); }); it('Should pass the query language to the language switcher', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: luceneQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + }) ); expect(component).toMatchSnapshot(); }); it('Should disable autoFocus on EuiFieldText when disableAutoFocus prop is true', () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + }) ); expect(component).toMatchSnapshot(); @@ -144,43 +149,32 @@ describe('QueryBarInput', () => { it('Should create a unique PersistedLog based on the appName and query language', () => { mockPersistedLogFactory.mockClear(); - mountWithIntl( - + mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + appName: 'discover', + }) ); - expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); }); it("On language selection, should store the user's preference in localstorage and reset the query", () => { const mockStorage = createMockStorage(); const mockCallback = jest.fn(); - - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext( + { + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + appName: 'discover', + }, + mockStorage + ) ); component @@ -194,23 +188,16 @@ describe('QueryBarInput', () => { it('Should call onSubmit when the user hits enter inside the query bar', () => { const mockCallback = jest.fn(); - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: mockCallback, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + }) ); - const instance = component.instance() as QueryBarInputUI; + const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -220,23 +207,17 @@ describe('QueryBarInput', () => { }); it('Should use PersistedLog for recent search suggestions', async () => { - const component = mountWithIntl( - + const component = mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: [mockIndexPattern], + disableAutoFocus: true, + persistedLog: mockPersistedLog, + }) ); - const instance = component.instance() as QueryBarInputUI; + const instance = component.find('QueryBarInputUI').instance() as QueryBarInputUI; const input = instance.inputRef; const inputWrapper = component.find(EuiFieldText).find('input'); inputWrapper.simulate('keyDown', { target: input, keyCode: 13, key: 'Enter', metaKey: true }); @@ -250,22 +231,15 @@ describe('QueryBarInput', () => { it('Should accept index pattern strings and fetch the full object', () => { mockFetchIndexPatterns.mockClear(); - - mountWithIntl( - + mount( + wrapQueryBarInputInContext({ + query: kqlQuery, + onSubmit: noop, + indexPatterns: ['logstash-*'], + disableAutoFocus: true, + }) ); + expect(mockFetchIndexPatterns).toHaveBeenCalledWith( startMock.savedObjects.client, ['logstash-*'], diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx index 7a972a6068f6f6..3cdd8d4b9c40c9 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx @@ -19,23 +19,22 @@ import { Component } from 'react'; import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiFieldText, EuiOutsideClickDetector, PopoverAnchorPosition } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { debounce, compact, isEqual, omit } from 'lodash'; +import { debounce, compact, isEqual } from 'lodash'; import { PersistedLog } from 'ui/persisted_log'; -import { Storage } from 'ui/storage'; -import { npStart } from 'ui/new_platform'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; + import { AutocompleteSuggestion, AutocompleteSuggestionType, } from '../../../../../../../plugins/data/public'; +import { + withKibana, + KibanaReactContextValue, +} from '../../../../../../../plugins/kibana_react/public'; import { IndexPattern, StaticIndexPattern } from '../../../index_patterns'; import { Query } from '../index'; import { fromUser, matchPairs, toUser } from '../lib'; @@ -43,21 +42,13 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { SuggestionsComponent } from './typeahead/suggestions_component'; import { getQueryLog } from '../lib/get_query_log'; import { fetchIndexPatterns } from '../lib/fetch_index_patterns'; - -// todo: related to https://github.com/elastic/kibana/pull/45762/files -// Will be refactored after merge of related PR -const getAutocompleteProvider = (language: string) => - npStart.plugins.data.autocomplete.getProvider(language); +import { IDataPluginServices } from '../../../types'; interface Props { - uiSettings: UiSettingsClientContract; - indexPatterns: Array; - savedObjectsClient: SavedObjectsClientContract; - http: HttpServiceBase; - store: Storage; + kibana: KibanaReactContextValue; intl: InjectedIntl; + indexPatterns: Array; query: Query; - appName: string; disableAutoFocus?: boolean; screenTitle?: string; prepend?: React.ReactNode; @@ -67,6 +58,7 @@ interface Props { languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; onChange?: (query: Query) => void; onSubmit?: (query: Query) => void; + dataTestSubj?: string; } interface State { @@ -107,6 +99,7 @@ export class QueryBarInputUI extends Component { public inputRef: HTMLInputElement | null = null; private persistedLog: PersistedLog | undefined; + private services = this.props.kibana.services; private componentIsUnmounting = false; private getQueryString = () => { @@ -122,9 +115,9 @@ export class QueryBarInputUI extends Component { ) as IndexPattern[]; const objectPatternsFromStrings = (await fetchIndexPatterns( - this.props.savedObjectsClient, + this.services.savedObjects!.client, stringPatterns, - this.props.uiSettings + this.services.uiSettings! )) as IndexPattern[]; this.setState({ @@ -137,13 +130,13 @@ export class QueryBarInputUI extends Component { return; } - const uiSettings = this.props.uiSettings; + const uiSettings = this.services.uiSettings; const language = this.props.query.language; const queryString = this.getQueryString(); const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString); - const autocompleteProvider = getAutocompleteProvider(language); + const autocompleteProvider = this.services.autocomplete.getProvider(language); if ( !autocompleteProvider || !Array.isArray(this.state.indexPatterns) || @@ -369,11 +362,11 @@ export class QueryBarInputUI extends Component { // Send telemetry info every time the user opts in or out of kuery // As a result it is important this function only ever gets called in the // UI component's change handler. - this.props.http.post('/api/kibana/kql_opt_in_telemetry', { + this.services.http.post('/api/kibana/kql_opt_in_telemetry', { body: JSON.stringify({ opt_in: language === 'kuery' }), }); - this.props.store.set('kibana.userQueryLanguage', language); + this.services.store.set('kibana.userQueryLanguage', language); const newQuery = { query: '', language }; this.onChange(newQuery); @@ -406,7 +399,7 @@ export class QueryBarInputUI extends Component { this.persistedLog = this.props.persistedLog ? this.props.persistedLog - : getQueryLog(this.props.uiSettings, this.props.appName, this.props.query.language); + : getQueryLog(this.services.uiSettings, this.services.appName, this.props.query.language); this.fetchIndexPatterns().then(this.updateSuggestions); } @@ -419,7 +412,7 @@ export class QueryBarInputUI extends Component { this.persistedLog = this.props.persistedLog ? this.props.persistedLog - : getQueryLog(this.props.uiSettings, this.props.appName, this.props.query.language); + : getQueryLog(this.services.uiSettings, this.services.appName, this.props.query.language); if (!isEqual(prevProps.indexPatterns, this.props.indexPatterns)) { this.fetchIndexPatterns().then(this.updateSuggestions); @@ -446,41 +439,30 @@ export class QueryBarInputUI extends Component { } public render() { - const rest = omit(this.props, [ - 'indexPatterns', - 'intl', - 'query', - 'appName', - 'disableAutoFocus', - 'screenTitle', - 'prepend', - 'store', - 'persistedLog', - 'bubbleSubmitEvent', - 'languageSwitcherPopoverAnchorPosition', - 'onChange', - 'onSubmit', - 'uiSettings', - 'savedObjectsClient', - ]); + const isSuggestionsVisible = this.state.isSuggestionsVisible && { + 'aria-controls': 'kbnTypeahead__items', + 'aria-owns': 'kbnTypeahead__items', + }; + const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' }; return (
{ }} autoComplete="off" spellCheck={false} - aria-label={ - this.props.screenTitle - ? this.props.intl.formatMessage( - { - id: 'data.query.queryBar.searchInputAriaLabel', - defaultMessage: - 'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}', - }, - { - previouslyTranslatedPageTitle: this.props.screenTitle, - pageType: this.props.appName, - } - ) - : undefined - } + aria-label={i18n.translate('data.query.queryBar.searchInputAriaLabel', { + defaultMessage: 'Start typing to search and filter the {pageType} page', + values: { pageType: this.services.appName }, + })} type="text" - data-test-subj="queryInput" aria-autocomplete="list" - aria-controls="kbnTypeahead__items" + aria-controls={this.state.isSuggestionsVisible ? 'kbnTypeahead__items' : undefined} aria-activedescendant={ - this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' + this.state.isSuggestionsVisible && typeof this.state.index === 'number' + ? `suggestion-${this.state.index}` + : undefined } role="textbox" prepend={this.props.prepend} @@ -529,7 +501,7 @@ export class QueryBarInputUI extends Component { onSelectLanguage={this.onSelectLanguage} /> } - {...rest} + data-test-subj={this.props.dataTestSubj || 'queryInput'} />
@@ -548,4 +520,4 @@ export class QueryBarInputUI extends Component { } } -export const QueryBarInput = injectI18n(QueryBarInputUI); +export const QueryBarInput = injectI18n(withKibana(QueryBarInputUI)); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx index 0926af7b30ef78..337bb9f4861c3e 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.tsx @@ -21,7 +21,6 @@ import { mockPersistedLogFactory } from './query_bar_input.test.mocks'; import React from 'react'; import { mount } from 'enzyme'; -import './query_bar_top_row.test.mocks'; import { QueryBarTopRow } from './query_bar_top_row'; import { IndexPattern } from '../../../index'; @@ -97,42 +96,49 @@ const mockIndexPattern = { ], } as IndexPattern; -describe('QueryBarTopRowTopRow', () => { - const QUERY_INPUT_SELECTOR = 'InjectIntl(QueryBarInputUI)'; - const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; +function wrapQueryBarTopRowInContext(testProps: any) { + const defaultOptions = { + screenTitle: 'Another Screen', + onSubmit: noop, + onChange: noop, + intl: null as any, + }; + const services = { + appName: 'discover', uiSettings: startMock.uiSettings, savedObjects: startMock.savedObjects, notifications: startMock.notifications, http: startMock.http, - }; - const defaultOptions = { - appName: 'discover', - screenTitle: 'Another Screen', - onSubmit: noop, - onChange: noop, store: createMockStorage(), - intl: null as any, }; + return ( + + + + + + ); +} + +describe('QueryBarTopRowTopRow', () => { + const QUERY_INPUT_SELECTOR = 'QueryBarInputUI'; + const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker'; + beforeEach(() => { jest.clearAllMocks(); }); it('Should render the given query', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + screenTitle: 'Another Screen', + isDirty: false, + indexPatterns: [mockIndexPattern], + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1); @@ -141,19 +147,14 @@ describe('QueryBarTopRowTopRow', () => { it('Should create a unique PersistedLog based on the appName and query language', () => { mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + screenTitle: 'Another Screen', + indexPatterns: [mockIndexPattern], + timeHistory: timefilterSetupMock.history, + disableAutoFocus: true, + isDirty: false, + }) ); expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery'); @@ -161,15 +162,10 @@ describe('QueryBarTopRowTopRow', () => { it('Should render only timepicker when no options provided', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -178,16 +174,11 @@ describe('QueryBarTopRowTopRow', () => { it('Should not show timepicker when asked', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + isDirty: false, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -196,19 +187,14 @@ describe('QueryBarTopRowTopRow', () => { it('Should render timepicker with options', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: true, + dateRangeFrom: 'now-7d', + dateRangeTo: 'now', + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -217,19 +203,16 @@ describe('QueryBarTopRowTopRow', () => { it('Should render only query input bar', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + indexPatterns: [mockIndexPattern], + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: false, + dateRangeFrom: 'now-7d', + dateRangeTo: 'now', + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1); @@ -238,20 +221,15 @@ describe('QueryBarTopRowTopRow', () => { it('Should NOT render query input bar if disabled', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + query: kqlQuery, + isDirty: false, + screenTitle: 'Another Screen', + indexPatterns: [mockIndexPattern], + showQueryInput: false, + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); @@ -260,17 +238,12 @@ describe('QueryBarTopRowTopRow', () => { it('Should NOT render query input bar if missing options', () => { const component = mount( - - - - - + wrapQueryBarTopRowInContext({ + isDirty: false, + screenTitle: 'Another Screen', + showDatePicker: false, + timeHistory: timefilterSetupMock.history, + }) ); expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index c25b596973174d..6895c9ecd018cb 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -21,7 +21,6 @@ import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query'; import classNames from 'classnames'; import React, { useState, useEffect } from 'react'; -import { Storage } from 'ui/storage'; import { documentationLinks } from 'ui/documentation_links'; import { PersistedLog } from 'ui/persisted_log'; @@ -30,6 +29,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSuperDatePicker } fro import { EuiSuperUpdateButton } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; +import { TimeRange } from 'src/plugins/data/public'; import { useKibana } from '../../../../../../../plugins/kibana_react/public'; import { IndexPattern } from '../../../index_patterns'; @@ -37,21 +37,15 @@ import { QueryBarInput } from './query_bar_input'; import { getQueryLog } from '../lib/get_query_log'; import { Query } from '../index'; import { TimeHistoryContract } from '../../../timefilter'; - -interface DateRange { - from: string; - to: string; -} +import { IDataPluginServices } from '../../../types'; interface Props { query?: Query; - onSubmit: (payload: { dateRange: DateRange; query?: Query }) => void; - onChange: (payload: { dateRange: DateRange; query?: Query }) => void; + onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; disableAutoFocus?: boolean; - appName: string; screenTitle?: string; indexPatterns?: Array; - store?: Storage; intl: InjectedIntl; prepend?: React.ReactNode; showQueryInput?: boolean; @@ -70,15 +64,15 @@ interface Props { function QueryBarTopRowUI(props: Props) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); - const kibana = useKibana(); - const { uiSettings, http, notifications, savedObjects } = kibana.services; + const kibana = useKibana(); + const { uiSettings, notifications, store, appName } = kibana.services; const queryLanguage = props.query && props.query.language; let persistedLog: PersistedLog | undefined; useEffect(() => { if (!props.query) return; - persistedLog = getQueryLog(uiSettings!, props.appName, props.query.language); + persistedLog = getQueryLog(uiSettings!, appName, props.query.language); }, [queryLanguage]); function onClickSubmitButton(event: React.MouseEvent) { @@ -131,7 +125,7 @@ function QueryBarTopRowUI(props: Props) { } } - function onSubmit({ query, dateRange }: { query?: Query; dateRange: DateRange }) { + function onSubmit({ query, dateRange }: { query?: Query; dateRange: TimeRange }) { handleLuceneSyntaxWarning(); if (props.timeHistory) { @@ -153,19 +147,14 @@ function QueryBarTopRowUI(props: Props) { return ( ); @@ -176,7 +165,7 @@ function QueryBarTopRowUI(props: Props) { } function shouldRenderQueryInput(): boolean { - return Boolean(props.showQueryInput && props.indexPatterns && props.query && props.store); + return Boolean(props.showQueryInput && props.indexPatterns && props.query && store); } function renderUpdateButton() { @@ -251,7 +240,7 @@ function QueryBarTopRowUI(props: Props) { function handleLuceneSyntaxWarning() { if (!props.query) return; - const { intl, store } = props; + const { intl } = props; const { query, language } = props.query; if ( language === 'kuery' && @@ -300,8 +289,8 @@ function QueryBarTopRowUI(props: Props) { } function onLuceneSyntaxWarningOptOut(toast: Toast) { - if (!props.store) return; - props.store.set('kibana.luceneSyntaxWarningOptOut', true); + if (!store) return; + store.set('kibana.luceneSyntaxWarningOptOut', true); notifications!.toasts.remove(toast); } diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx new file mode 100644 index 00000000000000..add49e47971d34 --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { Filter } from '@kbn/es-query'; +import { CoreStart } from 'src/core/public'; +import { Storage } from 'ui/storage'; +import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TimefilterSetup } from '../../../timefilter'; +import { FilterManager, SearchBar } from '../../../'; +import { SearchBarOwnProps } from '.'; + +interface StatefulSearchBarDeps { + core: CoreStart; + store: Storage; + timefilter: TimefilterSetup; + filterManager: FilterManager; + autocomplete: AutocompletePublicPluginStart; +} + +export type StatetfulSearchBarProps = SearchBarOwnProps & { + appName: string; +}; + +const defaultFiltersUpdated = (filterManager: FilterManager) => { + return (filters: Filter[]) => { + filterManager.setFilters(filters); + }; +}; + +const defaultOnRefreshChange = (timefilter: TimefilterSetup) => { + return (options: { isPaused: boolean; refreshInterval: number }) => { + timefilter.timefilter.setRefreshInterval({ + value: options.refreshInterval, + pause: options.isPaused, + }); + }; +}; + +export function createSearchBar({ + core, + store, + timefilter, + filterManager, + autocomplete, +}: StatefulSearchBarDeps) { + // App name should come from the core application service. + // Until it's available, we'll ask the user to provide it for the pre-wired component. + return (props: StatetfulSearchBarProps) => { + const timeRange = timefilter.timefilter.getTime(); + const refreshInterval = timefilter.timefilter.getRefreshInterval(); + + return ( + + + + ); + }; +} diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx index 24ffa939a746e9..accaac163acfcf 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx @@ -18,3 +18,4 @@ */ export * from './search_bar'; +export * from './create_search_bar'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx index 7d48977fab8a53..73e81a38572c39 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx @@ -18,14 +18,17 @@ */ import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { SearchBar } from './search_bar'; import { IndexPattern } from '../../../index_patterns'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; + import { coreMock } from '../../../../../../../../src/core/public/mocks'; const startMock = coreMock.createStart(); import { timefilterServiceMock } from '../../../timefilter/timefilter_service.mock'; +import { mount } from 'enzyme'; const timefilterSetupMock = timefilterServiceMock.createSetupContract(); jest.mock('../../../../../data/public', () => { @@ -41,13 +44,6 @@ jest.mock('../../../query/query_bar', () => { }; }); -jest.mock('ui/notify', () => ({ - toastNotifications: { - addSuccess: () => {}, - addDanger: () => {}, - }, -})); - const noop = jest.fn(); const createMockWebStorage = () => ({ @@ -87,26 +83,44 @@ const kqlQuery = { language: 'kuery', }; -describe('SearchBar', () => { - const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.filterBar'; - const QUERY_BAR = '.queryBar'; - - const options = { +function wrapSearchBarInContext(testProps: any) { + const defaultOptions = { appName: 'test', - savedObjects: startMock.savedObjects, - notifications: startMock.notifications, timeHistory: timefilterSetupMock.history, intl: null as any, }; + const services = { + uiSettings: startMock.uiSettings, + savedObjects: startMock.savedObjects, + notifications: startMock.notifications, + http: startMock.http, + store: createMockStorage(), + }; + + return ( + + + + + + ); +} + +describe('SearchBar', () => { + const SEARCH_BAR_ROOT = '.globalQueryBar'; + const FILTER_BAR = '.filterBar'; + const QUERY_BAR = '.queryBar'; + beforeEach(() => { jest.clearAllMocks(); }); it('Should render query bar when no options provided (in reality - timepicker)', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -115,12 +129,11 @@ describe('SearchBar', () => { }); it('Should render empty when timepicker is off and no options provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showDatePicker: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -129,14 +142,13 @@ describe('SearchBar', () => { }); it('Should render filter bar, when required fields are provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showDatePicker: false, + onFiltersUpdated: noop, + filters: [], + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -145,15 +157,14 @@ describe('SearchBar', () => { }); it('Should NOT render filter bar, if disabled', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + showFilterBar: false, + filters: [], + onFiltersUpdated: noop, + showDatePicker: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -162,15 +173,13 @@ describe('SearchBar', () => { }); it('Should render query bar, when required fields are provided', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -179,16 +188,14 @@ describe('SearchBar', () => { }); it('Should NOT render query bar, if disabled', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + showQueryBar: false, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); @@ -197,17 +204,15 @@ describe('SearchBar', () => { }); it('Should render query bar and filter bar', () => { - const component = mountWithIntl( - + const component = mount( + wrapSearchBarInContext({ + indexPatterns: [mockIndexPattern], + screenTitle: 'test screen', + onQuerySubmit: noop, + query: kqlQuery, + filters: [], + onFiltersUpdated: noop, + }) ); expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index 6c73fa3614cc3c..06ceace7e9e44a 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -22,10 +22,9 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; -import { Storage } from 'ui/storage'; import { get, isEqual } from 'lodash'; -import { CoreStart } from 'src/core/public'; +import { TimeRange } from 'src/plugins/data/common/types'; import { IndexPattern, Query, FilterBar } from '../../../../../data/public'; import { QueryBarTopRow } from '../../../query'; import { SavedQuery, SavedQueryAttributes } from '../index'; @@ -34,54 +33,56 @@ import { SavedQueryManagementComponent } from './saved_query_management/saved_qu import { SavedQueryService } from '../lib/saved_query_service'; import { createSavedQueryService } from '../lib/saved_query_service'; import { TimeHistoryContract } from '../../../timefilter'; - -interface DateRange { - from: string; - to: string; -} - -/** - * NgReact lib requires that changes to the props need to be made in the directive config as well - * See [search_bar\directive\index.js] file - */ -export interface SearchBarProps { - appName: string; +import { + withKibana, + KibanaReactContextValue, +} from '../../../../../../../plugins/kibana_react/public'; +import { IDataPluginServices } from '../../../types'; + +interface SearchBarInjectedDeps { + kibana: KibanaReactContextValue; intl: InjectedIntl; - indexPatterns?: IndexPattern[]; - - // Query bar - showQueryBar?: boolean; - showQueryInput?: boolean; - screenTitle?: string; - store?: Storage; - query?: Query; - savedQuery?: SavedQuery; - onQuerySubmit?: (payload: { dateRange: DateRange; query?: Query }) => void; timeHistory: TimeHistoryContract; // Filter bar - showFilterBar?: boolean; - filters?: Filter[]; onFiltersUpdated?: (filters: Filter[]) => void; + filters?: Filter[]; // Date picker - showDatePicker?: boolean; dateRangeFrom?: string; dateRangeTo?: string; // Autorefresh + onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; isRefreshPaused?: boolean; refreshInterval?: number; +} + +export interface SearchBarOwnProps { + indexPatterns?: IndexPattern[]; + customSubmitButton?: React.ReactNode; + screenTitle?: string; + + // Togglers + showQueryBar?: boolean; + showQueryInput?: boolean; + showFilterBar?: boolean; + showDatePicker?: boolean; showAutoRefreshOnly?: boolean; - showSaveQuery?: boolean; onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; + // Query bar - should be in SearchBarInjectedDeps + query?: Query; + // Show when user has privileges to save + showSaveQuery?: boolean; + savedQuery?: SavedQuery; + onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; + // User has saved the current state as a saved query onSaved?: (savedQuery: SavedQuery) => void; + // User has modified the saved query, your app should persist the update onSavedQueryUpdated?: (savedQuery: SavedQuery) => void; + // User has cleared the active query, your app should clear the entire query bar onClearSavedQuery?: () => void; - customSubmitButton?: React.ReactNode; - - // TODO: deprecate - savedObjects: CoreStart['savedObjects']; - notifications: CoreStart['notifications']; } +export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; + interface State { isFiltersVisible: boolean; showSaveQueryModal: boolean; @@ -102,7 +103,7 @@ class SearchBarUI extends Component { }; private savedQueryService!: SavedQueryService; - + private services = this.props.kibana.services; public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; @@ -253,7 +254,7 @@ class SearchBarUI extends Component { response = await this.savedQueryService.saveQuery(savedQueryAttributes); } - this.props.notifications.toasts.addSuccess( + this.services.notifications.toasts.addSuccess( `Your query "${response.attributes.title}" was saved` ); @@ -266,7 +267,7 @@ class SearchBarUI extends Component { this.props.onSaved(response); } } catch (error) { - this.props.notifications.toasts.addDanger( + this.services.notifications.toasts.addDanger( `An error occured while saving your query: ${error.message}` ); throw error; @@ -285,7 +286,7 @@ class SearchBarUI extends Component { }); }; - public onQueryBarChange = (queryAndDateRange: { dateRange: DateRange; query?: Query }) => { + public onQueryBarChange = (queryAndDateRange: { dateRange: TimeRange; query?: Query }) => { this.setState({ query: queryAndDateRange.query, dateRangeFrom: queryAndDateRange.dateRange.from, @@ -293,7 +294,7 @@ class SearchBarUI extends Component { }); }; - public onQueryBarSubmit = (queryAndDateRange: { dateRange?: DateRange; query?: Query }) => { + public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { this.setState( { query: queryAndDateRange.query, @@ -337,8 +338,8 @@ class SearchBarUI extends Component { this.setFilterBarHeight(); this.ro.observe(this.filterBarRef); } - if (this.props.savedObjects) { - this.savedQueryService = createSavedQueryService(this.props.savedObjects!.client); + if (this.services.savedObjects) { + this.savedQueryService = createSavedQueryService(this.services.savedObjects.client); } } @@ -370,9 +371,7 @@ class SearchBarUI extends Component { query={this.state.query} screenTitle={this.props.screenTitle} onSubmit={this.onQueryBarSubmit} - appName={this.props.appName} indexPatterns={this.props.indexPatterns} - store={this.props.store} prepend={this.props.showFilterBar ? savedQueryManagement : undefined} showDatePicker={this.props.showDatePicker} dateRangeFrom={this.state.dateRangeFrom} @@ -449,4 +448,4 @@ class SearchBarUI extends Component { } } -export const SearchBar = injectI18n(SearchBarUI); +export const SearchBar = injectI18n(withKibana(SearchBarUI)); diff --git a/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts index 4289d56b33c605..126754388f13f2 100644 --- a/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts +++ b/src/legacy/core_plugins/data/public/shim/legacy_dependencies_plugin.ts @@ -18,7 +18,8 @@ */ import chrome from 'ui/chrome'; -import { CoreStart, Plugin } from '../../../../../../src/core/public'; +import { Storage } from 'ui/storage'; +import { Plugin } from '../../../../../../src/core/public'; import { initLegacyModule } from './legacy_module'; /** @internal */ @@ -26,6 +27,10 @@ export interface LegacyDependenciesPluginSetup { savedObjectsClient: any; } +export interface LegacyDependenciesPluginStart { + storage: Storage; +} + export class LegacyDependenciesPlugin implements Plugin { public setup() { initLegacyModule(); @@ -35,7 +40,9 @@ export class LegacyDependenciesPlugin implements Plugin { } as LegacyDependenciesPluginSetup; } - public start(core: CoreStart) { - // nothing to do here yet + public start() { + return { + storage: new Storage(window.localStorage), + } as LegacyDependenciesPluginStart; } } diff --git a/src/legacy/core_plugins/data/public/timefilter/get_time.ts b/src/legacy/core_plugins/data/public/timefilter/get_time.ts index e54725dd9ba486..18a43d789714d7 100644 --- a/src/legacy/core_plugins/data/public/timefilter/get_time.ts +++ b/src/legacy/core_plugins/data/public/timefilter/get_time.ts @@ -18,8 +18,8 @@ */ import dateMath from '@elastic/datemath'; -import { Field, IndexPattern } from 'ui/index_patterns'; import { TimeRange } from 'src/plugins/data/public'; +import { IndexPattern, Field } from '../index_patterns'; interface CalculateBoundsOptions { forceNow?: Date; diff --git a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts index c08ea9043da927..64129ea2af5ffb 100644 --- a/src/legacy/core_plugins/data/public/timefilter/timefilter.ts +++ b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts @@ -25,7 +25,7 @@ import { IndexPattern, TimeHistoryContract } from '../index'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; import { parseQueryString } from './lib/parse_querystring'; import { calculateBounds, getTime } from './get_time'; -import { TimefilterConfig, InputTimeRange } from './types'; +import { TimefilterConfig, InputTimeRange, TimeRangeBounds } from './types'; export class Timefilter { // Fired when isTimeRangeSelectorEnabled \ isAutoRefreshSelectorEnabled are toggled @@ -148,19 +148,19 @@ export class Timefilter { return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow()); }; - public getBounds = () => { + public getBounds(): TimeRangeBounds { return this.calculateBounds(this._time); - }; + } - public calculateBounds = (timeRange: TimeRange) => { + public calculateBounds(timeRange: TimeRange): TimeRangeBounds { return calculateBounds(timeRange, { forceNow: this.getForceNow() }); - }; + } - public getActiveBounds = () => { + public getActiveBounds(): TimeRangeBounds | undefined { if (this.isTimeRangeSelectorEnabled) { return this.getBounds(); } - }; + } /** * Show the time bounds selector part of the time filter diff --git a/src/legacy/core_plugins/data/public/types.ts b/src/legacy/core_plugins/data/public/types.ts new file mode 100644 index 00000000000000..4b7a5c1402ea72 --- /dev/null +++ b/src/legacy/core_plugins/data/public/types.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 { UiSettingsClientContract, CoreStart } from 'src/core/public'; +import { AutocompletePublicPluginStart } from 'src/plugins/data/public'; + +export interface IDataPluginServices extends Partial { + appName: string; + uiSettings: UiSettingsClientContract; + savedObjects: CoreStart['savedObjects']; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + store: Storage; + autocomplete: AutocompletePublicPluginStart; +} diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts index eeee5f3f4c6c71..4cbb1c82cc1e40 100644 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ b/src/legacy/core_plugins/elasticsearch/index.d.ts @@ -482,7 +482,7 @@ export interface CallCluster { (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - // ingest namepsace + // ingest namespace (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; diff --git a/src/legacy/core_plugins/input_control_vis/public/register_vis.js b/src/legacy/core_plugins/input_control_vis/public/register_vis.js index 76320b331bb066..731cf2dac9dd2e 100644 --- a/src/legacy/core_plugins/input_control_vis/public/register_vis.js +++ b/src/legacy/core_plugins/input_control_vis/public/register_vis.js @@ -24,7 +24,7 @@ import { OptionsTab } from './components/editor/options_tab'; import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; import { Status } from 'ui/vis/update_status'; import { i18n } from '@kbn/i18n'; -import { setup as visualizations } from '../../visualizations/public/legacy'; +import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy'; function InputControlVisProvider() { diff --git a/src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.js.snap b/src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.js.snap rename to src/legacy/core_plugins/interpreter/public/functions/__snapshots__/kibana.test.ts.snap diff --git a/src/legacy/core_plugins/interpreter/public/functions/__tests__/font.js b/src/legacy/core_plugins/interpreter/public/functions/__tests__/font.js deleted file mode 100644 index 4a7ebc1522f2ab..00000000000000 --- a/src/legacy/core_plugins/interpreter/public/functions/__tests__/font.js +++ /dev/null @@ -1,192 +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 expect from '@kbn/expect'; -import { openSans } from '../../../common/lib/fonts'; -import { font } from '../font'; -import { functionWrapper } from '../../../test_helpers'; - -describe('font', () => { - const fn = functionWrapper(font); - - describe('default output', () => { - const result = fn(null); - - it('returns a style', () => { - expect(result) - .to.have.property('type', 'style') - .and.to.have.property('spec') - .and.to.have.property('css'); - }); - }); - - describe('args', () => { - describe('size', () => { - it('sets font size', () => { - const result = fn(null, { size: 20 }); - expect(result.spec).to.have.property('fontSize', '20px'); - expect(result.css).to.contain('font-size:20px'); - }); - - it('defaults to 14px', () => { - const result = fn(null); - expect(result.spec).to.have.property('fontSize', '14px'); - expect(result.css).to.contain('font-size:14px'); - }); - }); - - describe('lHeight', () => { - it('sets line height', () => { - const result = fn(null, { lHeight: 30 }); - expect(result.spec).to.have.property('lineHeight', '30px'); - expect(result.css).to.contain('line-height:30px'); - }); - - it('defaults to 1', () => { - const result = fn(null); - expect(result.spec).to.have.property('lineHeight', '1'); - expect(result.css).to.contain('line-height:1'); - }); - }); - - describe('family', () => { - it('sets font family', () => { - const result = fn(null, { family: 'Optima, serif' }); - expect(result.spec).to.have.property('fontFamily', 'Optima, serif'); - expect(result.css).to.contain('font-family:Optima, serif'); - }); - - it(`defaults to "${openSans.value}"`, () => { - const result = fn(null); - expect(result.spec).to.have.property('fontFamily', `"${openSans.value}"`); - expect(result.css).to.contain(`font-family:"${openSans.value}"`); - }); - }); - - describe('color', () => { - it('sets font color', () => { - const result = fn(null, { color: 'blue' }); - expect(result.spec).to.have.property('color', 'blue'); - expect(result.css).to.contain('color:blue'); - }); - }); - - describe('weight', () => { - it('sets font weight', () => { - let result = fn(null, { weight: 'normal' }); - expect(result.spec).to.have.property('fontWeight', 'normal'); - expect(result.css).to.contain('font-weight:normal'); - - result = fn(null, { weight: 'bold' }); - expect(result.spec).to.have.property('fontWeight', 'bold'); - expect(result.css).to.contain('font-weight:bold'); - - result = fn(null, { weight: 'bolder' }); - expect(result.spec).to.have.property('fontWeight', 'bolder'); - expect(result.css).to.contain('font-weight:bolder'); - - result = fn(null, { weight: 'lighter' }); - expect(result.spec).to.have.property('fontWeight', 'lighter'); - expect(result.css).to.contain('font-weight:lighter'); - - result = fn(null, { weight: '400' }); - expect(result.spec).to.have.property('fontWeight', '400'); - expect(result.css).to.contain('font-weight:400'); - }); - - it('defaults to \'normal\'', () => { - const result = fn(null); - expect(result.spec).to.have.property('fontWeight', 'normal'); - expect(result.css).to.contain('font-weight:normal'); - }); - - it('throws when provided an invalid weight', () => { - expect(() => fn(null, { weight: 'foo' })).to.throwException(); - }); - }); - - describe('underline', () => { - it('sets text underline', () => { - let result = fn(null, { underline: true }); - expect(result.spec).to.have.property('textDecoration', 'underline'); - expect(result.css).to.contain('text-decoration:underline'); - - result = fn(null, { underline: false }); - expect(result.spec).to.have.property('textDecoration', 'none'); - expect(result.css).to.contain('text-decoration:none'); - }); - - it('defaults to false', () => { - const result = fn(null); - expect(result.spec).to.have.property('textDecoration', 'none'); - expect(result.css).to.contain('text-decoration:none'); - }); - }); - - describe('italic', () => { - it('sets italic', () => { - let result = fn(null, { italic: true }); - expect(result.spec).to.have.property('fontStyle', 'italic'); - expect(result.css).to.contain('font-style:italic'); - - result = fn(null, { italic: false }); - expect(result.spec).to.have.property('fontStyle', 'normal'); - expect(result.css).to.contain('font-style:normal'); - }); - - it('defaults to false', () => { - const result = fn(null); - expect(result.spec).to.have.property('fontStyle', 'normal'); - expect(result.css).to.contain('font-style:normal'); - }); - }); - - describe('align', () => { - it('sets text alignment', () => { - let result = fn(null, { align: 'left' }); - expect(result.spec).to.have.property('textAlign', 'left'); - expect(result.css).to.contain('text-align:left'); - - result = fn(null, { align: 'center' }); - expect(result.spec).to.have.property('textAlign', 'center'); - expect(result.css).to.contain('text-align:center'); - - result = fn(null, { align: 'right' }); - expect(result.spec).to.have.property('textAlign', 'right'); - expect(result.css).to.contain('text-align:right'); - - result = fn(null, { align: 'justify' }); - expect(result.spec).to.have.property('textAlign', 'justify'); - expect(result.css).to.contain('text-align:justify'); - }); - - it(`defaults to 'left'`, () => { - const result = fn(null); - expect(result.spec).to.have.property('textAlign', 'left'); - expect(result.css).to.contain('text-align:left'); - }); - - it('throws when provided an invalid alignment', () => { - expect(fn) - .withArgs(null, { align: 'foo' }) - .to.throwException(); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/clog.js b/src/legacy/core_plugins/interpreter/public/functions/clog.ts similarity index 91% rename from src/legacy/core_plugins/interpreter/public/functions/clog.js rename to src/legacy/core_plugins/interpreter/public/functions/clog.ts index 634d166f5f0bb5..4867726a42d72c 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/clog.js +++ b/src/legacy/core_plugins/interpreter/public/functions/clog.ts @@ -20,8 +20,8 @@ export const clog = () => ({ name: 'clog', help: 'Outputs the context to the console', - fn: context => { - console.log(context); //eslint-disable-line no-console + fn: (context: any) => { + console.log(context); // eslint-disable-line no-console return context; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/font.test.ts b/src/legacy/core_plugins/interpreter/public/functions/font.test.ts new file mode 100644 index 00000000000000..dba540178777bc --- /dev/null +++ b/src/legacy/core_plugins/interpreter/public/functions/font.test.ts @@ -0,0 +1,202 @@ +/* + * 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 { openSans } from '../../common/lib/fonts'; +import { font } from './font'; +import { functionWrapper } from '../../test_helpers'; + +describe('font', () => { + const fn: any = functionWrapper(font); + + describe('default output', () => { + const result = fn(null); + + it('returns a style', () => { + expect(result).toMatchObject({ + type: 'style', + spec: expect.any(Object), + css: expect.any(String), + }); + }); + }); + + describe('args', () => { + describe('size', () => { + it('sets font size', () => { + const result = fn(null, { size: 20 }); + expect(result).toMatchObject({ + spec: { + fontSize: '20px', + }, + }); + expect(result.css).toContain('font-size:20px'); + }); + + it('defaults to 14px', () => { + const result = fn(null); + expect(result).toMatchObject({ + spec: { + fontSize: '14px', + }, + }); + expect(result.css).toContain('font-size:14px'); + }); + }); + + describe('lHeight', () => { + it('sets line height', () => { + const result = fn(null, { lHeight: 30 }); + expect(result).toMatchObject({ + spec: { + lineHeight: '30px', + }, + }); + expect(result.css).toContain('line-height:30px'); + }); + + it('defaults to 1', () => { + const result = fn(null); + expect(result.spec.lineHeight).toBe('1'); + expect(result.css).toContain('line-height:1'); + }); + }); + + describe('family', () => { + it('sets font family', () => { + const result = fn(null, { family: 'Optima, serif' }); + expect(result.spec.fontFamily).toBe('Optima, serif'); + expect(result.css).toContain('font-family:Optima, serif'); + }); + + it(`defaults to "${openSans.value}"`, () => { + const result = fn(null); + expect(result.spec.fontFamily).toBe(`"${openSans.value}"`); + expect(result.css).toContain(`font-family:"${openSans.value}"`); + }); + }); + + describe('color', () => { + it('sets font color', () => { + const result = fn(null, { color: 'blue' }); + expect(result.spec.color).toBe('blue'); + expect(result.css).toContain('color:blue'); + }); + }); + + describe('weight', () => { + it('sets font weight', () => { + let result = fn(null, { weight: 'normal' }); + expect(result.spec.fontWeight).toBe('normal'); + expect(result.css).toContain('font-weight:normal'); + + result = fn(null, { weight: 'bold' }); + expect(result.spec.fontWeight).toBe('bold'); + expect(result.css).toContain('font-weight:bold'); + + result = fn(null, { weight: 'bolder' }); + expect(result.spec.fontWeight).toBe('bolder'); + expect(result.css).toContain('font-weight:bolder'); + + result = fn(null, { weight: 'lighter' }); + expect(result.spec.fontWeight).toBe('lighter'); + expect(result.css).toContain('font-weight:lighter'); + + result = fn(null, { weight: '400' }); + expect(result.spec.fontWeight).toBe('400'); + expect(result.css).toContain('font-weight:400'); + }); + + it("defaults to 'normal'", () => { + const result = fn(null); + expect(result.spec.fontWeight).toBe('normal'); + expect(result.css).toContain('font-weight:normal'); + }); + + it('throws when provided an invalid weight', () => { + expect(() => fn(null, { weight: 'foo' })).toThrow(); + }); + }); + + describe('underline', () => { + it('sets text underline', () => { + let result = fn(null, { underline: true }); + expect(result.spec.textDecoration).toBe('underline'); + expect(result.css).toContain('text-decoration:underline'); + + result = fn(null, { underline: false }); + expect(result.spec.textDecoration).toBe('none'); + expect(result.css).toContain('text-decoration:none'); + }); + + it('defaults to false', () => { + const result = fn(null); + expect(result.spec.textDecoration).toBe('none'); + expect(result.css).toContain('text-decoration:none'); + }); + }); + + describe('italic', () => { + it('sets italic', () => { + let result = fn(null, { italic: true }); + expect(result.spec.fontStyle).toBe('italic'); + expect(result.css).toContain('font-style:italic'); + + result = fn(null, { italic: false }); + expect(result.spec.fontStyle).toBe('normal'); + expect(result.css).toContain('font-style:normal'); + }); + + it('defaults to false', () => { + const result = fn(null); + expect(result.spec.fontStyle).toBe('normal'); + expect(result.css).toContain('font-style:normal'); + }); + }); + + describe('align', () => { + it('sets text alignment', () => { + let result = fn(null, { align: 'left' }); + expect(result.spec.textAlign).toBe('left'); + expect(result.css).toContain('text-align:left'); + + result = fn(null, { align: 'center' }); + expect(result.spec.textAlign).toBe('center'); + expect(result.css).toContain('text-align:center'); + + result = fn(null, { align: 'right' }); + expect(result.spec.textAlign).toBe('right'); + expect(result.css).toContain('text-align:right'); + + result = fn(null, { align: 'justify' }); + expect(result.spec.textAlign).toBe('justify'); + expect(result.css).toContain('text-align:justify'); + }); + + it(`defaults to 'left'`, () => { + const result = fn(null); + expect(result.spec.textAlign).toBe('left'); + expect(result.css).toContain('text-align:left'); + }); + + it('throws when provided an invalid alignment', () => { + expect(() => fn(null, { align: 'foo' })).toThrow(); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/interpreter/public/functions/index.js b/src/legacy/core_plugins/interpreter/public/functions/index.ts similarity index 92% rename from src/legacy/core_plugins/interpreter/public/functions/index.js rename to src/legacy/core_plugins/interpreter/public/functions/index.ts index 38c3920f91bd29..d86f033acb3d13 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/index.js +++ b/src/legacy/core_plugins/interpreter/public/functions/index.ts @@ -27,5 +27,12 @@ import { visualization } from './visualization'; import { visDimension } from './vis_dimension'; export const functions = [ - clog, esaggs, font, kibana, kibanaContext, range, visualization, visDimension, + clog, + esaggs, + font, + kibana, + kibanaContext, + range, + visualization, + visDimension, ]; diff --git a/src/legacy/core_plugins/interpreter/public/functions/kibana.test.js b/src/legacy/core_plugins/interpreter/public/functions/kibana.test.ts similarity index 97% rename from src/legacy/core_plugins/interpreter/public/functions/kibana.test.js rename to src/legacy/core_plugins/interpreter/public/functions/kibana.test.ts index 4757b9b12b50da..9f80449ac36be4 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/kibana.test.js +++ b/src/legacy/core_plugins/interpreter/public/functions/kibana.test.ts @@ -22,9 +22,9 @@ import { kibana } from './kibana'; describe('interpreter/functions#kibana', () => { const fn = functionWrapper(kibana); - let context; - let initialContext; - let handlers; + let context: any; + let initialContext: any; + let handlers: any; beforeEach(() => { context = { timeRange: { from: '0', to: '1' } }; diff --git a/src/legacy/core_plugins/interpreter/public/functions/kibana.js b/src/legacy/core_plugins/interpreter/public/functions/kibana.ts similarity index 93% rename from src/legacy/core_plugins/interpreter/public/functions/kibana.js rename to src/legacy/core_plugins/interpreter/public/functions/kibana.ts index e0817d8e04b026..37ff337f58b8d4 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/kibana.js +++ b/src/legacy/core_plugins/interpreter/public/functions/kibana.ts @@ -24,10 +24,10 @@ export const kibana = () => ({ type: 'kibana_context', context: {}, help: i18n.translate('interpreter.functions.kibana.help', { - defaultMessage: 'Gets kibana global context' + defaultMessage: 'Gets kibana global context', }), args: {}, - fn(context, args, handlers) { + fn(context: any, args: any, handlers: any) { const initialContext = handlers.getInitialContext ? handlers.getInitialContext() : {}; if (context.query) { @@ -45,7 +45,7 @@ export const kibana = () => ({ type: 'kibana_context', query: initialContext.query, filters: initialContext.filters, - timeRange: timeRange, + timeRange, }; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/kibana_context.js b/src/legacy/core_plugins/interpreter/public/functions/kibana_context.ts similarity index 87% rename from src/legacy/core_plugins/interpreter/public/functions/kibana_context.js rename to src/legacy/core_plugins/interpreter/public/functions/kibana_context.ts index 7b7294a87831d6..2f2241a3670945 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/kibana_context.js +++ b/src/legacy/core_plugins/interpreter/public/functions/kibana_context.ts @@ -24,13 +24,10 @@ export const kibanaContext = () => ({ name: 'kibana_context', type: 'kibana_context', context: { - types: [ - 'kibana_context', - 'null', - ], + types: ['kibana_context', 'null'], }, help: i18n.translate('interpreter.functions.kibana_context.help', { - defaultMessage: 'Updates kibana global context' + defaultMessage: 'Updates kibana global context', }), args: { q: { @@ -49,11 +46,11 @@ export const kibanaContext = () => ({ savedSearchId: { types: ['string', 'null'], default: null, - } + }, }, - async fn(context, args) { + async fn(context: any, args: any) { const $injector = await chrome.dangerouslyGetActiveInjector(); - const savedSearches = $injector.get('savedSearches'); + const savedSearches = $injector.get('savedSearches') as any; const queryArg = args.q ? JSON.parse(args.q) : []; let queries = Array.isArray(queryArg) ? queryArg : [queryArg]; let filters = args.filters ? JSON.parse(args.filters) : []; @@ -71,7 +68,7 @@ export const kibanaContext = () => ({ } if (context.filters) { - filters = filters.concat(context.filters).filter(f => !f.meta.disabled); + filters = filters.concat(context.filters).filter((f: any) => !f.meta.disabled); } const timeRange = args.timeRange ? JSON.parse(args.timeRange) : context.timeRange; @@ -79,8 +76,8 @@ export const kibanaContext = () => ({ return { type: 'kibana_context', query: queries, - filters: filters, - timeRange: timeRange, + filters, + timeRange, }; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.js b/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.ts similarity index 74% rename from src/legacy/core_plugins/interpreter/public/functions/vis_dimension.js rename to src/legacy/core_plugins/interpreter/public/functions/vis_dimension.ts index e1a6c41198bad4..19503dbe03ae98 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.js +++ b/src/legacy/core_plugins/interpreter/public/functions/vis_dimension.ts @@ -22,48 +22,48 @@ import { i18n } from '@kbn/i18n'; export const visDimension = () => ({ name: 'visdimension', help: i18n.translate('interpreter.function.visDimension.help', { - defaultMessage: 'Generates visConfig dimension object' + defaultMessage: 'Generates visConfig dimension object', }), type: 'vis_dimension', context: { - types: [ - 'kibana_datatable' - ], + types: ['kibana_datatable'], }, args: { accessor: { types: ['string', 'number'], aliases: ['_'], help: i18n.translate('interpreter.function.visDimension.accessor.help', { - defaultMessage: 'Column in your dataset to use (either column index or column name)' + defaultMessage: 'Column in your dataset to use (either column index or column name)', }), }, format: { types: ['string'], - default: 'string' + default: 'string', }, formatParams: { types: ['string'], default: '"{}"', - } + }, }, - fn: (context, args) => { - const accessor = Number.isInteger(args.accessor) ? - args.accessor : - context.columns.find(c => c.id === args.accessor); + fn: (context: any, args: any) => { + const accessor = Number.isInteger(args.accessor) + ? args.accessor + : context.columns.find((c: any) => c.id === args.accessor); if (accessor === undefined) { - throw new Error(i18n.translate('interpreter.function.visDimension.error.accessor', { - defaultMessage: 'Column name provided is invalid' - })); + throw new Error( + i18n.translate('interpreter.function.visDimension.error.accessor', { + defaultMessage: 'Column name provided is invalid', + }) + ); } return { type: 'vis_dimension', - accessor: accessor, + accessor, format: { id: args.format, params: JSON.parse(args.formatParams), - } + }, }; }, }); diff --git a/src/legacy/core_plugins/interpreter/public/functions/visualization.js b/src/legacy/core_plugins/interpreter/public/functions/visualization.ts similarity index 90% rename from src/legacy/core_plugins/interpreter/public/functions/visualization.js rename to src/legacy/core_plugins/interpreter/public/functions/visualization.ts index 7dceeaf6843540..d46044b544c419 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/visualization.js +++ b/src/legacy/core_plugins/interpreter/public/functions/visualization.ts @@ -20,17 +20,16 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; -import { setup as data } from '../../../data/public/legacy'; -import { start as visualizations } from '../../../visualizations/public/legacy'; - import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { PersistedState } from 'ui/persisted_state'; +import { setup as data } from '../../../data/public/legacy'; +import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; export const visualization = () => ({ name: 'visualization', type: 'render', help: i18n.translate('interpreter.functions.visualization.help', { - defaultMessage: 'A simple visualization' + defaultMessage: 'A simple visualization', }), args: { index: { @@ -60,17 +59,17 @@ export const visualization = () => ({ uiState: { types: ['string'], default: '"{}"', - } + }, }, - async fn(context, args, handlers) { + async fn(context: any, args: any, handlers: any) { const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); + const Private = $injector.get('Private') as any; const { indexPatterns } = data.indexPatterns; const queryFilter = Private(FilterBarQueryFilterProvider); const visConfigParams = JSON.parse(args.visConfig); const schemas = JSON.parse(args.schemas); - const visType = visualizations.types.get(args.type || 'histogram'); + const visType = visualizations.types.get(args.type || 'histogram') as any; const indexPattern = args.index ? await indexPatterns.get(args.index) : null; const uiStateParams = JSON.parse(args.uiState); @@ -85,7 +84,7 @@ export const visualization = () => ({ timeRange: get(context, 'timeRange', null), query: get(context, 'query', null), filters: get(context, 'filters', null), - uiState: uiState, + uiState, inspectorAdapters: handlers.inspectorAdapters, queryFilter, forceFetch: true, @@ -95,14 +94,14 @@ export const visualization = () => ({ if (typeof visType.responseHandler === 'function') { if (context.columns) { // assign schemas to aggConfigs - context.columns.forEach(column => { + context.columns.forEach((column: any) => { if (column.aggConfig) { column.aggConfig.aggConfigs.schemas = visType.schemas.all; } }); Object.keys(schemas).forEach(key => { - schemas[key].forEach(i => { + schemas[key].forEach((i: any) => { if (context.columns[i] && context.columns[i].aggConfig) { context.columns[i].aggConfig.schema = key; } @@ -119,8 +118,8 @@ export const visualization = () => ({ value: { visData: context, visType: args.type, - visConfig: visConfigParams - } + visConfig: visConfigParams, + }, }; - } + }, }); diff --git a/src/legacy/core_plugins/interpreter/public/interpreter.test.js b/src/legacy/core_plugins/interpreter/public/interpreter.test.ts similarity index 96% rename from src/legacy/core_plugins/interpreter/public/interpreter.test.js rename to src/legacy/core_plugins/interpreter/public/interpreter.test.ts index bd7dc0a47c1240..1de1e8c0cc0598 100644 --- a/src/legacy/core_plugins/interpreter/public/interpreter.test.js +++ b/src/legacy/core_plugins/interpreter/public/interpreter.test.ts @@ -24,9 +24,9 @@ jest.mock('ui/new_platform', () => ({ injectedMetadata: { getKibanaVersion: () => '8.0.0', getBasePath: () => '/lol', - } - } - } + }, + }, + }, })); jest.mock('uiExports/interpreter'); @@ -38,7 +38,7 @@ jest.mock('@kbn/interpreter/common', () => ({ const mockInterpreter = { interpreter: { interpretAst: jest.fn(), - } + }, }; jest.mock('./lib/interpreter', () => ({ initializeInterpreter: jest.fn().mockReturnValue(Promise.resolve(mockInterpreter)), @@ -57,9 +57,9 @@ jest.mock('./functions', () => ({ functions: [{}, {}, {}] })); jest.mock('./renderers/visualization', () => ({ visualization: {} })); describe('interpreter/interpreter', () => { - let getInterpreter; - let interpretAst; - let initializeInterpreter; + let getInterpreter: any; + let interpretAst: any; + let initializeInterpreter: any; beforeEach(() => { jest.clearAllMocks(); @@ -117,5 +117,4 @@ describe('interpreter/interpreter', () => { expect(mockInterpreter.interpreter.interpretAst).toHaveBeenCalledTimes(2); }); }); - }); diff --git a/src/legacy/core_plugins/interpreter/public/interpreter.js b/src/legacy/core_plugins/interpreter/public/interpreter.ts similarity index 87% rename from src/legacy/core_plugins/interpreter/public/interpreter.js rename to src/legacy/core_plugins/interpreter/public/interpreter.ts index 84e05bb10d9fa6..8ba82d5daf759a 100644 --- a/src/legacy/core_plugins/interpreter/public/interpreter.js +++ b/src/legacy/core_plugins/interpreter/public/interpreter.ts @@ -18,6 +18,7 @@ */ import 'uiExports/interpreter'; +// @ts-ignore import { register, registryFactory } from '@kbn/interpreter/common'; import { initializeInterpreter } from './lib/interpreter'; import { registries } from './registries'; @@ -27,7 +28,10 @@ import { typeSpecs } from '../../../../plugins/expressions/common'; // Expose kbnInterpreter.register(specs) and kbnInterpreter.registries() globally so that plugins // can register without a transpile step. -global.kbnInterpreter = Object.assign(global.kbnInterpreter || {}, registryFactory(registries)); +(global as any).kbnInterpreter = Object.assign( + (global as any).kbnInterpreter || {}, + registryFactory(registries) +); register(registries, { types: typeSpecs, @@ -35,7 +39,7 @@ register(registries, { renderers: [visualization], }); -let interpreterPromise; +let interpreterPromise: Promise | undefined; export const getInterpreter = async () => { if (!interpreterPromise) { @@ -44,7 +48,7 @@ export const getInterpreter = async () => { return await interpreterPromise; }; -export const interpretAst = async (...params) => { +export const interpretAst = async (...params: any) => { const { interpreter } = await getInterpreter(); return await interpreter.interpretAst(...params); }; diff --git a/src/legacy/core_plugins/interpreter/public/lib/render_function.js b/src/legacy/core_plugins/interpreter/public/lib/render_function.ts similarity index 92% rename from src/legacy/core_plugins/interpreter/public/lib/render_function.js rename to src/legacy/core_plugins/interpreter/public/lib/render_function.ts index 04aa05951be70f..76d1f58b661950 100644 --- a/src/legacy/core_plugins/interpreter/public/lib/render_function.js +++ b/src/legacy/core_plugins/interpreter/public/lib/render_function.ts @@ -17,7 +17,7 @@ * under the License. */ -export function RenderFunction(config) { +export function RenderFunction(this: any, config: any) { // This must match the name of the function that is used to create the `type: render` object this.name = config.name; @@ -36,7 +36,7 @@ export function RenderFunction(config) { // the function called to render the data this.render = config.render || - function render(domNode, data, done) { + function render(domNode: any, data: any, done: any) { done(); }; } diff --git a/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.js b/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.ts similarity index 88% rename from src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.js rename to src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.ts index 60e823baf0fa78..427e7f7454c24e 100644 --- a/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.js +++ b/src/legacy/core_plugins/interpreter/public/lib/render_functions_registry.ts @@ -20,9 +20,9 @@ import { Registry } from '@kbn/interpreter/common'; import { RenderFunction } from './render_function'; -class RenderFunctionsRegistry extends Registry { - wrapper(obj) { - return new RenderFunction(obj); +class RenderFunctionsRegistry extends Registry { + wrapper(obj: any) { + return new (RenderFunction as any)(obj); } } diff --git a/src/legacy/core_plugins/interpreter/public/renderers/visualization.js b/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts similarity index 84% rename from src/legacy/core_plugins/interpreter/public/renderers/visualization.js rename to src/legacy/core_plugins/interpreter/public/renderers/visualization.ts index 38fe02436380c7..960e925b132213 100644 --- a/src/legacy/core_plugins/interpreter/public/renderers/visualization.js +++ b/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts @@ -19,17 +19,18 @@ import chrome from 'ui/chrome'; import { visualizationLoader } from 'ui/visualize/loader/visualization_loader'; +// @ts-ignore import { VisProvider } from 'ui/visualize/loader/vis'; export const visualization = () => ({ name: 'visualization', displayName: 'visualization', reuseDomNode: true, - render: async (domNode, config, handlers) => { + render: async (domNode: HTMLElement, config: any, handlers: any) => { const { visData, visConfig, params } = config; const visType = config.visType || visConfig.type; const $injector = await chrome.dangerouslyGetActiveInjector(); - const Private = $injector.get('Private'); + const Private = $injector.get('Private') as any; const Vis = Private(VisProvider); if (handlers.vis) { @@ -49,8 +50,10 @@ export const visualization = () => ({ handlers.onDestroy(() => visualizationLoader.destroy()); - await visualizationLoader.render(domNode, handlers.vis, visData, handlers.vis.params, uiState, params).then(() => { - if (handlers.done) handlers.done(); - }); + await visualizationLoader + .render(domNode, handlers.vis, visData, handlers.vis.params, uiState, params) + .then(() => { + if (handlers.done) handlers.done(); + }); }, }); diff --git a/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.js b/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts similarity index 95% rename from src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.js rename to src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts index a6e0e13049e1c9..00886630807748 100644 --- a/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.js +++ b/src/legacy/core_plugins/interpreter/server/lib/__tests__/create_handlers.ts @@ -28,13 +28,13 @@ const mockServer = { plugins: { elasticsearch: { getCluster: () => ({ - callWithRequest: (...args) => Promise.resolve(args), + callWithRequest: (...args: any) => Promise.resolve(args), }), }, }, config: () => ({ has: () => false, - get: val => val, + get: (val: any) => val, }), info: { uri: 'serveruri', diff --git a/src/legacy/core_plugins/interpreter/server/lib/create_handlers.js b/src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts similarity index 88% rename from src/legacy/core_plugins/interpreter/server/lib/create_handlers.js rename to src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts index d4ea9b3dc6180f..6e295d0aecaa59 100644 --- a/src/legacy/core_plugins/interpreter/server/lib/create_handlers.js +++ b/src/legacy/core_plugins/interpreter/server/lib/create_handlers.ts @@ -17,7 +17,7 @@ * under the License. */ -export const createHandlers = (request, server) => { +export const createHandlers = (request: any, server: any) => { const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); const config = server.config(); @@ -27,6 +27,6 @@ export const createHandlers = (request, server) => { config.has('server.rewriteBasePath') && config.get('server.rewriteBasePath') ? `${server.info.uri}${config.get('server.basePath')}` : server.info.uri, - elasticsearchClient: async (...args) => callWithRequest(request, ...args), + elasticsearchClient: async (...args: any) => callWithRequest(request, ...args), }; }; diff --git a/src/legacy/core_plugins/interpreter/server/routes/index.js b/src/legacy/core_plugins/interpreter/server/routes/index.ts similarity index 95% rename from src/legacy/core_plugins/interpreter/server/routes/index.js rename to src/legacy/core_plugins/interpreter/server/routes/index.ts index 9140f93a9bde64..50385147dd38e6 100644 --- a/src/legacy/core_plugins/interpreter/server/routes/index.js +++ b/src/legacy/core_plugins/interpreter/server/routes/index.ts @@ -19,6 +19,6 @@ import { registerServerFunctions } from './server_functions'; -export function routes(server) { +export function routes(server: any) { registerServerFunctions(server); } diff --git a/src/legacy/core_plugins/interpreter/server/routes/server_functions.js b/src/legacy/core_plugins/interpreter/server/routes/server_functions.ts similarity index 84% rename from src/legacy/core_plugins/interpreter/server/routes/server_functions.js rename to src/legacy/core_plugins/interpreter/server/routes/server_functions.ts index b64a9af006e412..740b046610d9e9 100644 --- a/src/legacy/core_plugins/interpreter/server/routes/server_functions.js +++ b/src/legacy/core_plugins/interpreter/server/routes/server_functions.ts @@ -18,16 +18,16 @@ */ import Boom from 'boom'; +import Joi from 'joi'; import { serializeProvider, API_ROUTE } from '../../common'; import { createHandlers } from '../lib/create_handlers'; -import Joi from 'joi'; /** * Register the Canvas function endopints. * * @param {*} server - The Kibana server */ -export function registerServerFunctions(server) { +export function registerServerFunctions(server: any) { getServerFunctions(server); runServerFunctions(server); } @@ -37,7 +37,7 @@ export function registerServerFunctions(server) { * * @param {*} server - The Kibana server */ -function runServerFunctions(server) { +function runServerFunctions(server: any) { server.route({ method: 'POST', path: `${API_ROUTE}/fns`, @@ -48,19 +48,20 @@ function runServerFunctions(server) { }, validate: { payload: Joi.object({ - functions: Joi.array().items( - Joi.object() - .keys({ + functions: Joi.array() + .items( + Joi.object().keys({ id: Joi.number().required(), functionName: Joi.string().required(), args: Joi.object().default({}), context: Joi.any().default(null), - }), - ).required(), + }) + ) + .required(), }).required(), }, }, - async handler(req) { + async handler(req: any) { const handlers = await createHandlers(req, server); const { functions } = req.payload; @@ -73,19 +74,19 @@ function runServerFunctions(server) { // Send the initial headers. res.writeHead(200, { 'Content-Type': 'text/plain', - 'Connection': 'keep-alive', + Connection: 'keep-alive', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'no-cache', }); // Write a length-delimited response - const streamResult = (result) => { + const streamResult = (result: any) => { const payload = JSON.stringify(result) + '\n'; res.write(`${payload.length}:${payload}`); }; // Tries to run an interpreter function, and ensures a consistent error payload on failure. - const tryFunction = async (id, fnCall) => { + const tryFunction = async (id: any, fnCall: any) => { try { const result = await runFunction(server, handlers, fnCall); @@ -96,7 +97,7 @@ function runServerFunctions(server) { return { id, statusCode: 200, result }; } catch (err) { if (Boom.isBoom(err)) { - return batchError(id, err.output.payload, err.statusCode); + return batchError(id, err.output.payload, (err as any).statusCode); } else if (err instanceof Error) { return batchError(id, err.message); } @@ -107,7 +108,9 @@ function runServerFunctions(server) { }; // Process each function individually, and stream the responses back to the client - await Promise.all(functions.map(({ id, ...fnCall }) => tryFunction(id, fnCall).then(streamResult))); + await Promise.all( + functions.map(({ id, ...fnCall }: any) => tryFunction(id, fnCall).then(streamResult)) + ); // All of the responses have been written, so we can close the response. res.end(); @@ -118,7 +121,7 @@ function runServerFunctions(server) { /** * A helper function for bundling up errors. */ -function batchError(id, message, statusCode = 500) { +function batchError(id: any, message: any, statusCode = 500) { return { id, statusCode, @@ -130,7 +133,7 @@ function batchError(id, message, statusCode = 500) { * Register the endpoint that returns the list of server-only functions. * @param {*} server - The Kibana server */ -function getServerFunctions(server) { +function getServerFunctions(server: any) { server.route({ method: 'GET', path: `${API_ROUTE}/fns`, @@ -147,7 +150,7 @@ function getServerFunctions(server) { * @param {*} handlers - The Canvas handlers * @param {*} fnCall - Describes the function being run `{ functionName, args, context }` */ -async function runFunction(server, handlers, fnCall) { +async function runFunction(server: any, handlers: any, fnCall: any) { const registries = server.plugins.interpreter.registries(); const { functionName, args, context } = fnCall; const types = registries.types.toJS(); diff --git a/src/legacy/core_plugins/interpreter/test_helpers.js b/src/legacy/core_plugins/interpreter/test_helpers.ts similarity index 86% rename from src/legacy/core_plugins/interpreter/test_helpers.js rename to src/legacy/core_plugins/interpreter/test_helpers.ts index e743b8a09280e2..741cd83bb47fed 100644 --- a/src/legacy/core_plugins/interpreter/test_helpers.js +++ b/src/legacy/core_plugins/interpreter/test_helpers.ts @@ -21,8 +21,9 @@ import { mapValues } from 'lodash'; // Takes a function spec and passes in default args, // overriding with any provided args. -export const functionWrapper = fnSpec => { +export const functionWrapper = (fnSpec: any) => { const spec = fnSpec(); const defaultArgs = mapValues(spec.args, argSpec => argSpec.default); - return (context, args, handlers) => spec.fn(context, { ...defaultArgs, ...args }, handlers); + return (context: any, args: any, handlers: any) => + spec.fn(context, { ...defaultArgs, ...args }, handlers); }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx index a24a37ca971d58..b96132fa29380f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/labels_panel.tsx @@ -29,12 +29,12 @@ function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInter return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx index 4abfb2e604b1dc..4e3b511782c9e7 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/ranges_panel.tsx @@ -38,12 +38,12 @@ function RangesPanel({ return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx index f606080afbdb2f..a76171673d9a82 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/gauge/style_panel.tsx @@ -34,12 +34,12 @@ function StylePanel({ aggs, setGaugeValue, stateParams, vis }: GaugeOptionsInter return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap new file mode 100644 index 00000000000000..d88654cfdc0c43 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap @@ -0,0 +1,109 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CategoryAxisPanel component should init with the default set of props 1`] = ` + + +

+ +

+
+ + + + +
+`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap new file mode 100644 index 00000000000000..56f35ae0211732 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap @@ -0,0 +1,78 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChartOptions component should init with the default set of props 1`] = ` + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap new file mode 100644 index 00000000000000..c7d3f4036fa049 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CustomExtentsOptions component should init with the default set of props 1`] = ` + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000000..256df603a7f33a --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -0,0 +1,354 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MetricsAxisOptions component should init with the default set of props 1`] = ` + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap new file mode 100644 index 00000000000000..0dc2f6519a1084 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LabelOptions component should init with the default set of props 1`] = ` + + + +

+ +

+
+ + + + + + + + + + + + +
+`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap new file mode 100644 index 00000000000000..7b45423f5f861e --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LineOptions component should init with the default set of props 1`] = ` + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap new file mode 100644 index 00000000000000..8d20765fe35918 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap @@ -0,0 +1,502 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ValueAxesPanel component should init with the default set of props 1`] = ` + + + + +

+ +

+
+
+ + + + + +
+ + + ValueAxis-1 + + + + Count + + + + } + buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + className="visEditorSidebar__section visEditorSidebar__collapsible" + data-test-subj="toggleYAxisOptions-ValueAxis-1" + extraAction={ + + + + } + id="yAxisAccordionValueAxis-1" + initialIsOpen={false} + key="ValueAxis-1" + paddingSize="none" + > + + + + + ValueAxis-1 + + + + Average + + + + } + buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate" + className="visEditorSidebar__section visEditorSidebar__collapsible" + data-test-subj="toggleYAxisOptions-ValueAxis-2" + extraAction={ + + + + } + id="yAxisAccordionValueAxis-2" + initialIsOpen={false} + key="ValueAxis-2" + paddingSize="none" + > + + + +
+`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap new file mode 100644 index 00000000000000..f2ee088450fbdd --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap @@ -0,0 +1,415 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ValueAxisOptions component should init with the default set of props 1`] = ` + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap new file mode 100644 index 00000000000000..3372e781a028ae --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`YExtents component should init with the default set of props 1`] = ` + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.test.tsx new file mode 100644 index 00000000000000..a32e48baf4588c --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.test.tsx @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { CategoryAxisPanel, CategoryAxisPanelProps } from './category_axis_panel'; +import { Axis } from '../../../types'; +import { Positions, getPositions } from '../../../utils/collections'; +import { LabelOptions } from './label_options'; +import { categoryAxis } from './mocks'; + +const positions = getPositions(); + +describe('CategoryAxisPanel component', () => { + let setCategoryAxis: jest.Mock; + let onPositionChanged: jest.Mock; + let defaultProps: CategoryAxisPanelProps; + let axis: Axis; + + beforeEach(() => { + setCategoryAxis = jest.fn(); + onPositionChanged = jest.fn(); + axis = categoryAxis; + + defaultProps = { + axis, + vis: { + type: { + editorConfig: { + collections: { positions }, + }, + }, + }, + onPositionChanged, + setCategoryAxis, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should respond axis.show', () => { + const comp = shallow(); + + expect(comp.find(LabelOptions).exists()).toBeTruthy(); + + comp.setProps({ axis: { ...axis, show: false } }); + expect(comp.find(LabelOptions).exists()).toBeFalsy(); + }); + + it('should call onPositionChanged when position is changed', () => { + const value = Positions.RIGHT; + const comp = shallow(); + comp.find({ paramName: 'position' }).prop('setValue')('position', value); + + expect(setCategoryAxis).toHaveBeenLastCalledWith({ ...axis, position: value }); + expect(onPositionChanged).toBeCalledWith(value); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx index a2edc533e6e7e3..11946a5a6bccd6 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/category_axis_panel.tsx @@ -28,7 +28,7 @@ import { SelectOption, SwitchOption } from '../../common'; import { LabelOptions } from './label_options'; import { Positions } from '../../../utils/collections'; -interface CategoryAxisPanelProps extends VisOptionsProps { +export interface CategoryAxisPanelProps extends VisOptionsProps { axis: Axis; onPositionChanged: (position: Positions) => void; setCategoryAxis: (value: Axis) => void; @@ -59,12 +59,12 @@ function CategoryAxisPanel(props: CategoryAxisPanelProps) { return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.test.tsx new file mode 100644 index 00000000000000..ba1a46ba7d89e7 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.test.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ChartOptions, ChartOptionsParams } from './chart_options'; +import { SeriesParam } from '../../../types'; +import { LineOptions } from './line_options'; +import { + ChartTypes, + ChartModes, + getInterpolationModes, + getChartTypes, + getChartModes, +} from '../../../utils/collections'; +import { valueAxis, seriesParam } from './mocks'; + +const interpolationModes = getInterpolationModes(); +const chartTypes = getChartTypes(); +const chartModes = getChartModes(); + +describe('ChartOptions component', () => { + let setParamByIndex: jest.Mock; + let changeValueAxis: jest.Mock; + let defaultProps: ChartOptionsParams; + let chart: SeriesParam; + + beforeEach(() => { + setParamByIndex = jest.fn(); + changeValueAxis = jest.fn(); + chart = { ...seriesParam }; + + defaultProps = { + index: 0, + chart, + vis: { + type: { + editorConfig: { + collections: { interpolationModes, chartTypes, chartModes }, + }, + }, + }, + stateParams: { + valueAxes: [valueAxis], + }, + setParamByIndex, + changeValueAxis, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should show LineOptions when type is line', () => { + chart.type = ChartTypes.LINE; + const comp = shallow(); + + expect(comp.find(LineOptions).exists()).toBeTruthy(); + }); + + it('should show line mode when type is area', () => { + chart.type = ChartTypes.AREA; + const comp = shallow(); + + expect(comp.find({ paramName: 'interpolate' }).exists()).toBeTruthy(); + }); + + it('should call changeValueAxis when valueAxis is changed', () => { + const comp = shallow(); + const paramName = 'valueAxis'; + const value = 'new'; + comp.find({ paramName }).prop('setValue')(paramName, value); + + expect(changeValueAxis).toBeCalledWith(0, paramName, value); + }); + + it('should call setParamByIndex when mode is changed', () => { + const comp = shallow(); + const paramName = 'mode'; + comp.find({ paramName }).prop('setValue')(paramName, ChartModes.NORMAL); + + expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartModes.NORMAL); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx index ba41ad802ade4d..1c9357c67c2f08 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/chart_options.tsx @@ -30,7 +30,7 @@ import { SetParamByIndex, ChangeValueAxis } from './'; export type SetChart = (paramName: T, value: SeriesParam[T]) => void; -interface ChartOptionsParams extends VisOptionsProps { +export interface ChartOptionsParams extends VisOptionsProps { chart: SeriesParam; index: number; changeValueAxis: ChangeValueAxis; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.test.tsx new file mode 100644 index 00000000000000..b55d363fa56c8e --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.test.tsx @@ -0,0 +1,150 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { CustomExtentsOptions, CustomExtentsOptionsProps } from './custom_extents_options'; +import { YExtents } from './y_extents'; +import { valueAxis } from './mocks'; + +const BOUNDS_MARGIN = 'boundsMargin'; +const DEFAULT_Y_EXTENTS = 'defaultYExtents'; +const SCALE = 'scale'; +const SET_Y_EXTENTS = 'setYExtents'; + +describe('CustomExtentsOptions component', () => { + let setValueAxis: jest.Mock; + let setValueAxisScale: jest.Mock; + let setMultipleValidity: jest.Mock; + let defaultProps: CustomExtentsOptionsProps; + + beforeEach(() => { + setValueAxis = jest.fn(); + setValueAxisScale = jest.fn(); + setMultipleValidity = jest.fn(); + + defaultProps = { + axis: { ...valueAxis }, + setValueAxis, + setValueAxisScale, + setMultipleValidity, + }; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + describe('boundsMargin', () => { + it('should set validity as true when value is positive', () => { + const comp = shallow(); + comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, 5); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); + }); + + it('should set validity as true when value is empty', () => { + const comp = shallow(); + comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, ''); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); + }); + + it('should set validity as false when value is negative', () => { + defaultProps.axis.scale.defaultYExtents = true; + const comp = shallow(); + comp.find({ paramName: BOUNDS_MARGIN }).prop('setValue')(BOUNDS_MARGIN, -1); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, false); + }); + }); + + describe('defaultYExtents', () => { + it('should show bounds margin input when defaultYExtents is true', () => { + const comp = shallow(); + + expect(comp.find({ paramName: BOUNDS_MARGIN }).exists()).toBeTruthy(); + }); + + it('should hide bounds margin input when defaultYExtents is false', () => { + defaultProps.axis.scale = { ...defaultProps.axis.scale, defaultYExtents: false }; + const comp = shallow(); + + expect(comp.find({ paramName: BOUNDS_MARGIN }).exists()).toBeFalsy(); + }); + + it('should call setValueAxis when value is true', () => { + const comp = shallow(); + comp.find({ paramName: DEFAULT_Y_EXTENTS }).prop('setValue')(DEFAULT_Y_EXTENTS, true); + + expect(setMultipleValidity).not.toBeCalled(); + expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axis.scale); + }); + + it('should reset boundsMargin when value is false', () => { + const comp = shallow(); + comp.find({ paramName: DEFAULT_Y_EXTENTS }).prop('setValue')(DEFAULT_Y_EXTENTS, false); + + expect(setMultipleValidity).toBeCalledWith(BOUNDS_MARGIN, true); + const newScale = { + ...defaultProps.axis.scale, + boundsMargin: undefined, + defaultYExtents: false, + }; + expect(setValueAxis).toBeCalledWith(SCALE, newScale); + }); + }); + + describe('setYExtents', () => { + it('should show YExtents when value is true', () => { + const comp = shallow(); + + expect(comp.find(YExtents).exists()).toBeTruthy(); + }); + + it('should hide YExtents when value is false', () => { + defaultProps.axis.scale = { ...defaultProps.axis.scale, setYExtents: false }; + const comp = shallow(); + + expect(comp.find(YExtents).exists()).toBeFalsy(); + }); + + it('should call setValueAxis when value is true', () => { + const comp = shallow(); + comp.find({ paramName: SET_Y_EXTENTS }).prop('setValue')(SET_Y_EXTENTS, true); + + expect(setValueAxis).toBeCalledWith(SCALE, defaultProps.axis.scale); + }); + + it('should reset min and max when value is false', () => { + const comp = shallow(); + comp.find({ paramName: SET_Y_EXTENTS }).prop('setValue')(SET_Y_EXTENTS, false); + + const newScale = { + ...defaultProps.axis.scale, + min: undefined, + max: undefined, + setYExtents: false, + }; + expect(setValueAxis).toBeCalledWith(SCALE, newScale); + }); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx index dca948457b61c8..99783a6887716b 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/custom_extents_options.tsx @@ -25,7 +25,7 @@ import { NumberInputOption, SwitchOption } from '../../common'; import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; -interface CustomExtentsOptionsProps { +export interface CustomExtentsOptionsProps { axis: ValueAxis; setMultipleValidity(paramName: string, isValid: boolean): void; setValueAxis(paramName: T, value: ValueAxis[T]): void; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.test.tsx new file mode 100644 index 00000000000000..dc5cf422776034 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.test.tsx @@ -0,0 +1,314 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { MetricsAxisOptions } from './index'; +import { BasicVislibParams, SeriesParam, ValueAxis } from '../../../types'; +import { ValidationVisOptionsProps } from '../../common'; +import { Positions } from '../../../utils/collections'; +import { ValueAxesPanel } from './value_axes_panel'; +import { CategoryAxisPanel } from './category_axis_panel'; +import { ChartTypes } from '../../../utils/collections'; +import { AggConfig } from 'ui/vis'; +import { AggType } from 'ui/agg_types'; +import { defaultValueAxisId, valueAxis, seriesParam, categoryAxis } from './mocks'; + +jest.mock('./series_panel', () => ({ + SeriesPanel: () => 'SeriesPanel', +})); +jest.mock('./category_axis_panel', () => ({ + CategoryAxisPanel: () => 'CategoryAxisPanel', +})); +jest.mock('./value_axes_panel', () => ({ + ValueAxesPanel: () => 'ValueAxesPanel', +})); + +const SERIES_PARAMS = 'seriesParams'; +const VALUE_AXES = 'valueAxes'; + +const aggCount: AggConfig = { + id: '1', + type: { name: 'count' }, + makeLabel: () => 'Count', +} as AggConfig; + +const aggAverage: AggConfig = { + id: '2', + type: { name: 'average' } as AggType, + makeLabel: () => 'Average', +} as AggConfig; + +const createAggs = (aggs: any[]) => ({ + aggs, + bySchemaName: () => aggs, +}); + +describe('MetricsAxisOptions component', () => { + let setValue: jest.Mock; + let setVisType: jest.Mock; + let defaultProps: ValidationVisOptionsProps; + let axis: ValueAxis; + let axisRight: ValueAxis; + let chart: SeriesParam; + + beforeEach(() => { + setValue = jest.fn(); + setVisType = jest.fn(); + + axis = { + ...valueAxis, + name: 'LeftAxis-1', + position: Positions.LEFT, + }; + axisRight = { + ...valueAxis, + id: 'ValueAxis-2', + name: 'RightAxis-1', + position: Positions.RIGHT, + }; + chart = { + ...seriesParam, + type: ChartTypes.AREA, + }; + + defaultProps = { + aggs: createAggs([aggCount]), + aggsLabels: '', + vis: { + type: { + type: ChartTypes.AREA, + schemas: { metrics: [{ name: 'metric' }] }, + }, + }, + stateParams: { + valueAxes: [axis], + seriesParams: [chart], + categoryAxes: [categoryAxis], + grid: { valueAxis: defaultValueAxisId }, + }, + setValue, + setVisType, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + describe('useEffect', () => { + it('should update series when new agg is added', () => { + const comp = mount(); + comp.setProps({ + aggs: createAggs([aggCount, aggAverage]), + aggsLabels: `${aggCount.makeLabel()}, ${aggAverage.makeLabel()}`, + }); + + const updatedSeries = [chart, { ...chart, data: { id: '2', label: aggAverage.makeLabel() } }]; + expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); + }); + + it('should update series when new agg label is changed', () => { + const comp = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'New label' }; + comp.setProps({ + aggs: createAggs([agg]), + }); + + const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; + expect(setValue).toHaveBeenLastCalledWith(SERIES_PARAMS, updatedSeries); + }); + + it('should update visType when one seriesParam', () => { + const comp = mount(); + expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); + + comp.setProps({ + stateParams: { + ...defaultProps.stateParams, + seriesParams: [{ ...chart, type: ChartTypes.LINE }], + }, + }); + + expect(setVisType).toHaveBeenLastCalledWith(ChartTypes.LINE); + }); + + it('should set histogram visType when multiple seriesParam', () => { + const comp = mount(); + expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); + + comp.setProps({ + stateParams: { + ...defaultProps.stateParams, + seriesParams: [chart, { ...chart, type: ChartTypes.LINE }], + }, + }); + + expect(setVisType).toHaveBeenLastCalledWith(ChartTypes.HISTOGRAM); + }); + }); + + describe('updateAxisTitle', () => { + it('should not update the value axis title if custom title was set', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const comp = mount(); + const newAgg = { + ...aggCount, + makeLabel: () => 'Custom label', + }; + comp.setProps({ + aggs: createAggs([newAgg]), + aggsLabels: `${newAgg.makeLabel()}`, + }); + const updatedValues = [{ ...axis, title: { text: newAgg.makeLabel() } }]; + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should set the custom title to match the value axis label when only one agg exists for that axis', () => { + const comp = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should not set the custom title to match the value axis label when more than one agg exists for that axis', () => { + const comp = mount(); + const agg = { id: aggCount.id, makeLabel: () => 'Custom label' }; + comp.setProps({ + aggs: createAggs([agg, aggAverage]), + aggsLabels: `${agg.makeLabel()}, ${aggAverage.makeLabel()}`, + stateParams: { + ...defaultProps.stateParams, + seriesParams: [chart, chart], + }, + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + + it('should not overwrite the custom title with the value axis label if the custom title has been changed', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const comp = mount(); + const agg = { + id: aggCount.id, + params: { customLabel: 'Custom label' }, + makeLabel: () => 'Custom label', + }; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + expect(setValue).not.toHaveBeenCalledWith(VALUE_AXES); + }); + + it('should overwrite the custom title when the agg type changes', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const comp = mount(); + const agg = { + id: aggCount.id, + type: { name: 'max' }, + makeLabel: () => 'Max', + }; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + + it('should overwrite the custom title when the agg field changes', () => { + defaultProps.stateParams.valueAxes[0].title.text = 'Custom title'; + const agg = { + id: aggCount.id, + type: { name: 'max' }, + makeLabel: () => 'Max', + } as AggConfig; + defaultProps.aggs = createAggs([agg]) as any; + const comp = mount(); + agg.params = { field: { name: 'Field' } }; + agg.makeLabel = () => 'Max, Field'; + comp.setProps({ + aggs: createAggs([agg]), + aggsLabels: agg.makeLabel(), + }); + + const updatedValues = [{ ...axis, title: { text: agg.makeLabel() } }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); + }); + + it('should add value axis', () => { + const comp = shallow(); + comp.find(ValueAxesPanel).prop('addValueAxis')(); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axis, axisRight]); + }); + + describe('removeValueAxis', () => { + beforeEach(() => { + defaultProps.stateParams.valueAxes = [axis, axisRight]; + }); + + it('should remove value axis', () => { + const comp = shallow(); + comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, [axisRight]); + }); + + it('should update seriesParams "valueAxis" prop', () => { + const updatedSeriesParam = { ...chart, valueAxis: 'ValueAxis-2' }; + const comp = shallow(); + comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, [updatedSeriesParam]); + }); + + it('should reset grid "valueAxis" prop', () => { + const updatedGrid = { valueAxis: undefined }; + defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; + const comp = shallow(); + comp.find(ValueAxesPanel).prop('removeValueAxis')(axis); + + expect(setValue).toHaveBeenCalledWith('grid', updatedGrid); + }); + }); + + it('should update axis value when when category position chnaged', () => { + const comp = shallow(); + comp.find(CategoryAxisPanel).prop('onPositionChanged')(Positions.LEFT); + + const updatedValues = [{ ...axis, name: 'BottomAxis-1', position: Positions.BOTTOM }]; + expect(setValue).toHaveBeenCalledWith(VALUE_AXES, updatedValues); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx index 05797b8dde5b21..c7ada18f9e1f25 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx @@ -125,13 +125,17 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) lastLabels[axis.id] = newCustomLabel; if ( - aggTypeIsChanged || - aggFieldIsChanged || - axis.title.text === '' || - lastCustomLabels[axis.id] === axis.title.text + Object.keys(lastCustomLabels).length !== 0 && + (aggTypeIsChanged || + aggFieldIsChanged || + axis.title.text === '' || + lastCustomLabels[axis.id] === axis.title.text) ) { // Override axis title with new custom label - axes[axisNumber] = { ...axes[axisNumber], title: { ...axis, text: newCustomLabel } }; + axes[axisNumber] = { + ...axis, + title: { ...axis.title, text: newCustomLabel }, + }; isAxesChanged = true; } } diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.test.tsx new file mode 100644 index 00000000000000..abb3a2455f9f9e --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.test.tsx @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { LabelOptions, LabelOptionsProps } from './label_options'; +import { TruncateLabelsOption } from '../../common'; +import { valueAxis, categoryAxis } from './mocks'; + +const FILTER = 'filter'; +const ROTATE = 'rotate'; +const DISABLED = 'disabled'; +const CATEGORY_AXES = 'categoryAxes'; + +describe('LabelOptions component', () => { + let setValue: jest.Mock; + let defaultProps: LabelOptionsProps; + + beforeEach(() => { + setValue = jest.fn(); + + defaultProps = { + axis: { ...valueAxis }, + axesName: CATEGORY_AXES, + index: 0, + stateParams: { + categoryAxes: [{ ...categoryAxis }], + valueAxes: [{ ...valueAxis }], + } as any, + setValue, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should show other fields when axis.labels.show is true', () => { + const comp = shallow(); + + expect(comp.find({ paramName: FILTER }).prop(DISABLED)).toBeFalsy(); + expect(comp.find({ paramName: ROTATE }).prop(DISABLED)).toBeFalsy(); + expect(comp.find(TruncateLabelsOption).prop(DISABLED)).toBeFalsy(); + }); + + it('should disable other fields when axis.labels.show is false', () => { + defaultProps.axis.labels.show = false; + const comp = shallow(); + + expect(comp.find({ paramName: FILTER }).prop(DISABLED)).toBeTruthy(); + expect(comp.find({ paramName: ROTATE }).prop(DISABLED)).toBeTruthy(); + expect(comp.find(TruncateLabelsOption).prop(DISABLED)).toBeTruthy(); + }); + + it('should set rotate as number', () => { + const comp = shallow(); + comp.find({ paramName: ROTATE }).prop('setValue')(ROTATE, '5'); + + const newAxes = [{ ...categoryAxis, labels: { ...categoryAxis.labels, rotate: 5 } }]; + expect(setValue).toBeCalledWith(CATEGORY_AXES, newAxes); + }); + + it('should set filter value', () => { + const comp = shallow(); + expect(defaultProps.stateParams.categoryAxes[0].labels.filter).toBeTruthy(); + comp.find({ paramName: FILTER }).prop('setValue')(FILTER, false); + + const newAxes = [{ ...categoryAxis, labels: { ...categoryAxis.labels, filter: false } }]; + expect(setValue).toBeCalledWith(CATEGORY_AXES, newAxes); + }); + + it('should set value for valueAxes', () => { + defaultProps.axesName = 'valueAxes'; + const comp = shallow(); + comp.find(TruncateLabelsOption).prop('setValue')('truncate', 10); + + const newAxes = [{ ...valueAxis, labels: { ...valueAxis.labels, truncate: 10 } }]; + expect(setValue).toBeCalledWith('valueAxes', newAxes); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx index a0d91a0abe38af..9918cd8cd807cb 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/label_options.tsx @@ -27,7 +27,7 @@ import { BasicVislibParams, Axis } from '../../../types'; import { SelectOption, SwitchOption, TruncateLabelsOption } from '../../common'; import { getRotateOptions } from '../../../utils/collections'; -interface LabelOptionsProps extends VisOptionsProps { +export interface LabelOptionsProps extends VisOptionsProps { axis: Axis; axesName: 'categoryAxes' | 'valueAxes'; index: number; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.test.tsx new file mode 100644 index 00000000000000..0e603814493fa5 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; +import { LineOptions, LineOptionsParams } from './line_options'; +import { NumberInputOption } from '../../common'; +import { getInterpolationModes } from '../../../utils/collections'; +import { seriesParam } from './mocks'; + +const LINE_WIDTH = 'lineWidth'; +const DRAW_LINES = 'drawLinesBetweenPoints'; +const interpolationModes = getInterpolationModes(); + +describe('LineOptions component', () => { + let setChart: jest.Mock; + let defaultProps: LineOptionsParams; + + beforeEach(() => { + setChart = jest.fn(); + + defaultProps = { + chart: { ...seriesParam }, + vis: { + type: { + editorConfig: { + collections: { interpolationModes }, + }, + }, + }, + setChart, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should set lineWidth as undefined when empty value', () => { + const comp = shallow(); + comp.find(NumberInputOption).prop('setValue')(LINE_WIDTH, ''); + + expect(setChart).toBeCalledWith(LINE_WIDTH, undefined); + }); + + it('should set lineWidth value', () => { + const comp = shallow(); + comp.find(NumberInputOption).prop('setValue')(LINE_WIDTH, 5); + + expect(setChart).toBeCalledWith(LINE_WIDTH, 5); + }); + + it('should set drawLinesBetweenPoints', () => { + const comp = shallow(); + comp.find({ paramName: DRAW_LINES }).prop('setValue')(DRAW_LINES, false); + + expect(setChart).toBeCalledWith(DRAW_LINES, false); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx index 8a4c8cc05efab6..9514b69a20b04e 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/line_options.tsx @@ -26,7 +26,7 @@ import { SeriesParam } from '../../../types'; import { NumberInputOption, SelectOption, SwitchOption } from '../../common'; import { SetChart } from './chart_options'; -interface LineOptionsParams { +export interface LineOptionsParams { chart: SeriesParam; vis: Vis; setChart: SetChart; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/mocks.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/mocks.ts new file mode 100644 index 00000000000000..422ad3c88fe8a4 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/mocks.ts @@ -0,0 +1,87 @@ +/* + * 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 { Axis, ValueAxis, SeriesParam } from '../../../types'; +import { + ChartTypes, + ChartModes, + InterpolationModes, + ScaleTypes, + Positions, + AxisTypes, +} from '../../../utils/collections'; + +const defaultValueAxisId = 'ValueAxis-1'; + +const axis = { + show: true, + style: {}, + title: { + text: '', + }, + labels: { + show: true, + filter: true, + truncate: 0, + color: 'black', + }, +}; + +const categoryAxis: Axis = { + ...axis, + id: 'CategoryAxis-1', + type: AxisTypes.CATEGORY, + position: Positions.BOTTOM, + scale: { + type: ScaleTypes.LINEAR, + }, +}; + +const valueAxis: ValueAxis = { + ...axis, + id: defaultValueAxisId, + name: 'ValueAxis-1', + type: AxisTypes.VALUE, + position: Positions.LEFT, + scale: { + type: ScaleTypes.LINEAR, + boundsMargin: 1, + defaultYExtents: true, + min: 1, + max: 2, + setYExtents: true, + }, +}; + +const seriesParam: SeriesParam = { + show: true, + type: ChartTypes.HISTOGRAM, + mode: ChartModes.STACKED, + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: InterpolationModes.LINEAR, + valueAxis: defaultValueAxisId, +}; + +export { defaultValueAxisId, categoryAxis, valueAxis, seriesParam }; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx index 816b0bfeda598f..5a455f4adde31f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/series_panel.tsx @@ -27,7 +27,7 @@ import { BasicVislibParams } from '../../../types'; import { ChartOptions } from './chart_options'; import { SetParamByIndex, ChangeValueAxis } from './'; -interface SeriesPanelProps extends VisOptionsProps { +export interface SeriesPanelProps extends VisOptionsProps { changeValueAxis: ChangeValueAxis; setParamByIndex: SetParamByIndex; } @@ -38,12 +38,12 @@ function SeriesPanel(props: SeriesPanelProps) { return ( -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.test.tsx new file mode 100644 index 00000000000000..080c64db7ff851 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.test.tsx @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ValueAxesPanel, ValueAxesPanelProps } from './value_axes_panel'; +import { ValueAxis, SeriesParam } from '../../../types'; +import { Positions, getScaleTypes, getAxisModes, getPositions } from '../../../utils/collections'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { valueAxis, seriesParam } from './mocks'; + +const positions = getPositions(); +const axisModes = getAxisModes(); +const scaleTypes = getScaleTypes(); + +describe('ValueAxesPanel component', () => { + let setParamByIndex: jest.Mock; + let onValueAxisPositionChanged: jest.Mock; + let setMultipleValidity: jest.Mock; + let addValueAxis: jest.Mock; + let removeValueAxis: jest.Mock; + let defaultProps: ValueAxesPanelProps; + let axisLeft: ValueAxis; + let axisRight: ValueAxis; + let seriesParamCount: SeriesParam; + let seriesParamAverage: SeriesParam; + + beforeEach(() => { + setParamByIndex = jest.fn(); + onValueAxisPositionChanged = jest.fn(); + addValueAxis = jest.fn(); + removeValueAxis = jest.fn(); + setMultipleValidity = jest.fn(); + axisLeft = { ...valueAxis }; + axisRight = { + ...valueAxis, + id: 'ValueAxis-2', + position: Positions.RIGHT, + }; + seriesParamCount = { ...seriesParam }; + seriesParamAverage = { + ...seriesParam, + valueAxis: 'ValueAxis-2', + data: { + label: 'Average', + id: '1', + }, + }; + + defaultProps = { + stateParams: { + seriesParams: [seriesParamCount, seriesParamAverage], + valueAxes: [axisLeft, axisRight], + }, + vis: { + type: { + editorConfig: { + collections: { scaleTypes, axisModes, positions }, + }, + }, + }, + isCategoryAxisHorizontal: false, + setParamByIndex, + onValueAxisPositionChanged, + addValueAxis, + removeValueAxis, + setMultipleValidity, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should not allow to remove the last value axis', () => { + defaultProps.stateParams.valueAxes = [axisLeft]; + const comp = mountWithIntl(); + expect(comp.find('[data-test-subj="removeValueAxisBtn"] button').exists()).toBeFalsy(); + }); + + it('should display remove button when multiple axes', () => { + const comp = mountWithIntl(); + + expect(comp.find('[data-test-subj="removeValueAxisBtn"] button').exists()).toBeTruthy(); + }); + + it('should call removeAgg', () => { + const comp = mountWithIntl(); + comp + .find('[data-test-subj="removeValueAxisBtn"] button') + .first() + .simulate('click'); + + expect(removeValueAxis).toBeCalledWith(axisLeft); + }); + + it('should call addValueAxis', () => { + const comp = mountWithIntl(); + comp.find('[data-test-subj="visualizeAddYAxisButton"] button').simulate('click'); + + expect(addValueAxis).toBeCalled(); + }); + + describe('description', () => { + it('should show when one serie matches value axis', () => { + const comp = mountWithIntl(); + expect( + comp + .find('.visEditorSidebar__aggGroupAccordionButtonContent span') + .first() + .text() + ).toBe(seriesParamCount.data.label); + }); + + it('should show when multiple series match value axis', () => { + defaultProps.stateParams.seriesParams[1].valueAxis = 'ValueAxis-1'; + const comp = mountWithIntl(); + expect( + comp + .find('.visEditorSidebar__aggGroupAccordionButtonContent span') + .first() + .text() + ).toBe(`${seriesParamCount.data.label}, ${seriesParamAverage.data.label}`); + }); + + it('should not show when no series match value axis', () => { + defaultProps.stateParams.seriesParams[0].valueAxis = 'ValueAxis-2'; + const comp = mountWithIntl(); + expect( + comp + .find('.visEditorSidebar__aggGroupAccordionButtonContent span') + .first() + .text() + ).toBe(''); + }); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx index 2ae54f9e093373..eb0ab4333af599 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axes_panel.tsx @@ -36,7 +36,7 @@ import { ValueAxisOptions } from './value_axis_options'; import { SetParamByIndex } from './'; import { ValidationVisOptionsProps } from '../../common'; -interface ValueAxesPanelProps extends ValidationVisOptionsProps { +export interface ValueAxesPanelProps extends ValidationVisOptionsProps { isCategoryAxisHorizontal: boolean; addValueAxis: () => ValueAxis; removeValueAxis: (axis: ValueAxis) => void; @@ -74,6 +74,7 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { iconType="cross" onClick={() => removeValueAxis(axis)} aria-label={removeButtonTooltip} + data-test-subj="removeValueAxisBtn" /> ), @@ -108,12 +109,12 @@ function ValueAxesPanel(props: ValueAxesPanelProps) { -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axis_options.test.tsx new file mode 100644 index 00000000000000..8cb476508c78b1 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/value_axis_options.test.tsx @@ -0,0 +1,165 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; +import { Axis } from '../../../types'; +import { TextInputOption } from '../../common'; +import { LabelOptions } from './label_options'; +import { + ScaleTypes, + Positions, + getScaleTypes, + getAxisModes, + getPositions, +} from '../../../utils/collections'; +import { valueAxis, categoryAxis } from './mocks'; + +const POSITION = 'position'; +const positions = getPositions(); +const axisModes = getAxisModes(); +const scaleTypes = getScaleTypes(); + +interface PositionOption { + text: string; + value: Positions; + disabled: boolean; +} + +describe('ValueAxisOptions component', () => { + let setParamByIndex: jest.Mock; + let onValueAxisPositionChanged: jest.Mock; + let setMultipleValidity: jest.Mock; + let defaultProps: ValueAxisOptionsParams; + let axis: Axis; + + beforeEach(() => { + setParamByIndex = jest.fn(); + setMultipleValidity = jest.fn(); + onValueAxisPositionChanged = jest.fn(); + axis = { ...valueAxis }; + + defaultProps = { + axis, + index: 0, + stateParams: { + categoryAxes: [{ ...categoryAxis }], + valueAxes: [axis], + }, + vis: { + type: { + editorConfig: { + collections: { scaleTypes, axisModes, positions }, + }, + }, + }, + isCategoryAxisHorizontal: false, + setParamByIndex, + onValueAxisPositionChanged, + setMultipleValidity, + } as any; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should hide options when axis.show is false', () => { + defaultProps.axis.show = false; + const comp = shallow(); + + expect(comp.find(TextInputOption).exists()).toBeFalsy(); + expect(comp.find(LabelOptions).exists()).toBeFalsy(); + }); + + it('should only allow left and right value axis position when category axis is horizontal', () => { + defaultProps.isCategoryAxisHorizontal = true; + const comp = shallow(); + + const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); + + expect(options.length).toBe(4); + options.forEach(({ value, disabled }) => { + switch (value) { + case Positions.LEFT: + case Positions.RIGHT: + expect(disabled).toBeFalsy(); + break; + case Positions.TOP: + case Positions.BOTTOM: + expect(disabled).toBeTruthy(); + break; + } + }); + }); + + it('should only allow top and bottom value axis position when category axis is vertical', () => { + defaultProps.isCategoryAxisHorizontal = false; + const comp = shallow(); + + const options: PositionOption[] = comp.find({ paramName: POSITION }).prop('options'); + + expect(options.length).toBe(4); + options.forEach(({ value, disabled }) => { + switch (value) { + case Positions.LEFT: + case Positions.RIGHT: + expect(disabled).toBeTruthy(); + break; + case Positions.TOP: + case Positions.BOTTOM: + expect(disabled).toBeFalsy(); + break; + } + }); + }); + + it('should call onValueAxisPositionChanged when position is changed', () => { + const value = Positions.RIGHT; + const comp = shallow(); + comp.find({ paramName: POSITION }).prop('setValue')(POSITION, value); + + expect(onValueAxisPositionChanged).toBeCalledWith(defaultProps.index, value); + }); + + it('should call setValueAxis when title is changed', () => { + defaultProps.axis.show = true; + const textValue = 'New title'; + const comp = shallow(); + comp.find(TextInputOption).prop('setValue')('text', textValue); + + expect(setParamByIndex).toBeCalledWith('valueAxes', defaultProps.index, 'title', { + text: textValue, + }); + }); + + it('should call setValueAxis when scale value is changed', () => { + const scaleValue = ScaleTypes.SQUARE_ROOT; + const comp = shallow(); + comp.find({ paramName: 'type' }).prop('setValue')('type', scaleValue); + + expect(setParamByIndex).toBeCalledWith('valueAxes', defaultProps.index, 'scale', { + ...defaultProps.axis.scale, + type: scaleValue, + }); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.test.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.test.tsx new file mode 100644 index 00000000000000..2df17b6e349852 --- /dev/null +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.test.tsx @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { YExtents, YExtentsProps } from './y_extents'; +import { ScaleTypes } from '../../../utils/collections'; +import { NumberInputOption } from '../../common'; + +describe('YExtents component', () => { + let setMultipleValidity: jest.Mock; + let setScale: jest.Mock; + let defaultProps: YExtentsProps; + const Y_EXTENTS = 'yExtents'; + + beforeEach(() => { + setMultipleValidity = jest.fn(); + setScale = jest.fn(); + + defaultProps = { + scale: { + type: ScaleTypes.LINEAR, + }, + setMultipleValidity, + setScale, + }; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should call setMultipleValidity with true when min and max are not defined', () => { + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, true); + }); + + it('should call setMultipleValidity with true when min less than max', () => { + defaultProps.scale.min = 1; + defaultProps.scale.max = 2; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, true); + }); + + it('should call setMultipleValidity with false when min greater than max', () => { + defaultProps.scale.min = 1; + defaultProps.scale.max = 0; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); + }); + + it('should call setMultipleValidity with false when min equals max', () => { + defaultProps.scale.min = 1; + defaultProps.scale.max = 1; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); + }); + + it('should call setMultipleValidity with false when min equals 0 and scale is log', () => { + defaultProps.scale.min = 0; + defaultProps.scale.max = 1; + defaultProps.scale.type = ScaleTypes.LOG; + mount(); + + expect(setMultipleValidity).toBeCalledWith(Y_EXTENTS, false); + }); + + it('should call setScale with input number', () => { + const inputNumber = 5; + const comp = shallow(); + const inputProps = comp + .find(NumberInputOption) + .first() + .props(); + inputProps.setValue(Y_EXTENTS, inputNumber); + + expect(setScale).toBeCalledWith(Y_EXTENTS, inputNumber); + }); + + it('should call setScale with null when input is empty', () => { + const comp = shallow(); + const inputProps = comp + .find(NumberInputOption) + .first() + .props(); + inputProps.setValue(Y_EXTENTS, ''); + + expect(setScale).toBeCalledWith(Y_EXTENTS, null); + }); +}); diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx index 29b986367d72ab..a5318f0ec3f00f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/y_extents.tsx @@ -49,7 +49,7 @@ function isNullOrUndefined(value?: number | null): value is null | undefined { return value === null || value === undefined; } -interface YExtentsProps { +export interface YExtentsProps { scale: Scale; setScale: SetScale; setMultipleValidity: (paramName: string, isValid: boolean) => void; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx index 982c7265d5494c..53dde185ec09fe 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/pie.tsx @@ -36,12 +36,12 @@ function PieOptions(props: VisOptionsProps) { <> -

+

-

+
) { -
+

-

+
-

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx index 11034f7f7335e4..8e3f66d12b9bdf 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/point_series.tsx @@ -34,12 +34,12 @@ function PointSeriesOptions(props: VisOptionsProps) { <> -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx index 9877b84345a1fe..49e56e377a8d56 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/point_series/threshold_panel.tsx @@ -42,12 +42,12 @@ function ThresholdPanel({ stateParams, setValue, vis }: VisOptionsProps -

+

-

+
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js index b11f21a96f38da..fe2cca3b800641 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/kbn_vislib_vis_types.js @@ -17,7 +17,7 @@ * under the License. */ -import { setup as visualizations } from '../../visualizations/public/legacy'; +import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy'; import histogramVisTypeProvider from './histogram'; import lineVisTypeProvider from './line'; diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts b/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts index f47c62e8b0fd24..84b5cb5285948f 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/utils/collections.ts @@ -330,4 +330,8 @@ export { getPositions, getRotateOptions, getScaleTypes, + getInterpolationModes, + getChartTypes, + getChartModes, + getAxisModes, }; diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js index 678a533fa8efcb..ff847bea4bee63 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/url.js @@ -189,5 +189,29 @@ describe('UrlFormat', function () { expect(converter('../foo/bar', null, null, parsedUrl)) .to.be('../foo/bar'); }); + + it('should support multiple types of urls w/o basePath', function () { + const url = new UrlFormat(); + const parsedUrl = { + origin: 'http://kibana.host.com', + pathname: '/app/kibana', + }; + const converter = url.getConverterFor('html'); + + expect(converter('10.22.55.66', null, null, parsedUrl)) + .to.be('10.22.55.66'); + + expect(converter('http://www.domain.name/app/kibana#/dashboard/', null, null, parsedUrl)) + .to.be('http://www.domain.name/app/kibana#/dashboard/'); + + expect(converter('/app/kibana', null, null, parsedUrl)) + .to.be('/app/kibana'); + + expect(converter('kibana#/dashboard/', null, null, parsedUrl)) + .to.be('kibana#/dashboard/'); + + expect(converter('#/dashboard/', null, null, parsedUrl)) + .to.be('#/dashboard/'); + }); }); }); diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js b/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts similarity index 82% rename from src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js rename to src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts index 0e1e1ecf058b9e..c232f65143d859 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/boolean.ts @@ -17,11 +17,19 @@ * under the License. */ -import { asPrettyString } from '../../../../../../plugins/data/common/field_formats'; +import { + FieldFormat, + asPrettyString, + KBN_FIELD_TYPES, +} from '../../../../../../plugins/data/common'; -export function createBoolFormat(FieldFormat) { +export function createBoolFormat() { return class BoolFormat extends FieldFormat { - _convert(value) { + static id = 'boolean'; + static title = 'Boolean'; + static fieldType = [KBN_FIELD_TYPES.BOOLEAN, KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING]; + + _convert(value: any): string { if (typeof value === 'string') { value = value.trim().toLowerCase(); } @@ -41,9 +49,5 @@ export function createBoolFormat(FieldFormat) { return asPrettyString(value); } } - - static id = 'boolean'; - static title = 'Boolean'; - static fieldType = ['boolean', 'number', 'string']; }; } diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/url.js b/src/legacy/core_plugins/kibana/common/field_formats/types/url.js index 4171e511f27f5d..63924727ab8bbf 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/url.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/url.js @@ -157,7 +157,9 @@ export function createUrlFormat(FieldFormat) { } // Handle urls like: `../app/kibana` else { - prefix = `${parsedUrl.origin}${parsedUrl.basePath}/app/`; + const prefixEnd = url[0] === '/' ? '' : '/'; + + prefix = `${parsedUrl.origin}${parsedUrl.basePath || ''}/app${prefixEnd}`; } } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html index 5ceb28e6b225b8..39db357a69321f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -120,6 +120,7 @@
+

{{screenTitle}}

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 index 741931af11c7de..22f127d12c4387 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -331,7 +331,8 @@ export class DashboardAppController { getDashboardTitle( dashboardStateManager.getTitle(), dashboardStateManager.getViewMode(), - dashboardStateManager.getIsDirty(timefilter) + dashboardStateManager.getIsDirty(timefilter), + dashboardStateManager.isNew() ); // Push breadcrumbs to new header navigation 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 c1ce5b764f2f6a..7c1fc771de3491 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 @@ -229,6 +229,14 @@ export class DashboardStateManager { return this.appState.title; } + public isSaved() { + return !!this.savedDashboard.id; + } + + public isNew() { + return !this.isSaved(); + } + public getDescription() { return this.appState.description; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts index b7f9293539abde..d932116d08dc8f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_strings.ts @@ -27,22 +27,31 @@ import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; * end of the title. * @returns {string} A title to display to the user based on the above parameters. */ -export function getDashboardTitle(title: string, viewMode: ViewMode, isDirty: boolean): string { +export function getDashboardTitle( + title: string, + viewMode: ViewMode, + isDirty: boolean, + isNew: boolean +): string { const isEditMode = viewMode === ViewMode.EDIT; let displayTitle: string; + const newDashboardTitle = i18n.translate('kbn.dashboard.savedDashboard.newDashboardTitle', { + defaultMessage: 'New Dashboard', + }); + const dashboardTitle = isNew ? newDashboardTitle : title; if (isEditMode && isDirty) { displayTitle = i18n.translate('kbn.dashboard.strings.dashboardUnsavedEditTitle', { defaultMessage: 'Editing {title} (unsaved)', - values: { title }, + values: { title: dashboardTitle }, }); } else if (isEditMode) { displayTitle = i18n.translate('kbn.dashboard.strings.dashboardEditTitle', { defaultMessage: 'Editing {title}', - values: { title }, + values: { title: dashboardTitle }, }); } else { - displayTitle = title; + displayTitle = dashboardTitle; } return displayTitle; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.js b/src/legacy/core_plugins/kibana/public/dashboard/index.js index 6b7b086b162214..712e05c92e5e8b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.js @@ -33,13 +33,14 @@ import { DashboardConstants, createDashboardEditUrl } from './dashboard_constant import { InvalidJSONProperty, SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { recentlyAccessed } from 'ui/persisted_log'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { uiModules } from 'ui/modules'; import 'ui/capabilities/route_setup'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; +import { npStart } from 'ui/new_platform'; + // load directives import '../../../data/public'; @@ -159,7 +160,7 @@ uiRoutes return savedDashboards.get(id) .then((savedDashboard) => { - recentlyAccessed.add(savedDashboard.getFullPath(), savedDashboard.title, id); + npStart.core.chrome.recentlyAccessed.add(savedDashboard.getFullPath(), savedDashboard.title, id); return savedDashboard; }) .catch((error) => { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 9b819443808c93..89b8e2ac83ec10 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -13,7 +13,6 @@ exports[`after fetch hideWriteControls 1`] = ` noItemsFragment={
@@ -106,7 +105,6 @@ exports[`after fetch initialFilter 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -199,7 +197,6 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -292,7 +289,6 @@ exports[`after fetch renders table rows 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -385,7 +381,6 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

@@ -478,7 +473,6 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = `

} - iconColor="subdued" iconType="dashboardApp" title={

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 36a083ef5a39ce..fe9e7b18d50072 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 @@ -18,7 +18,6 @@ */ import angular from 'angular'; -import { i18n } from '@kbn/i18n'; import { uiModules } from 'ui/modules'; import { createDashboardEditUrl } from '../dashboard_constants'; import { createLegacyClass } from 'ui/utils/legacy_class'; @@ -50,7 +49,7 @@ module.factory('SavedDashboard', function (Private) { // default values that will get assigned if the doc is new defaults: { - title: i18n.translate('kbn.dashboard.savedDashboard.newDashboardTitle', { defaultMessage: 'New Dashboard' }), + title: '', hits: 0, description: '', panelsJSON: '[]', diff --git a/src/legacy/core_plugins/kibana/public/discover/_discover.scss b/src/legacy/core_plugins/kibana/public/discover/_discover.scss index abf24241071c21..12cac1c89275b3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_discover.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_discover.scss @@ -19,9 +19,15 @@ discover-app { margin-top: 5px; } -// SASSTODO: replace the padding-top value with a variable .dscFieldList--popular { - padding-top: 10px; + padding-top: $euiSizeS; +} + +.dscFieldList--selected, +.dscFieldList--unpopular, +.dscFieldList--popular { + padding-left: $euiSizeS; + padding-right: $euiSizeS; } // SASSTODO: replace the z-index value with a variable @@ -151,9 +157,16 @@ discover-app { } } -// SASSTODO: replace the padding value with a variable +.dscFieldSearch { + padding: $euiSizeS; +} + +.dscFieldFilter { + margin-top: $euiSizeS; +} + .dscFieldDetails { - padding: 10px; + padding: $euiSizeS; background-color: $euiColorLightestShade; color: $euiTextColor; } diff --git a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss index cdc8e04dff5789..baf27bb9f82da1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_hacks.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_hacks.scss @@ -3,18 +3,4 @@ overflow: hidden; } -// SASSTODO: these are Angular Bootstrap classes. Will be replaced by EUI -.dscFieldDetails { - .progress { - background-color: shade($euiColorLightestShade, 5%); - margin-bottom: 0; - border-radius: 0; - } - .progress-bar { - padding-left: 10px; - text-align: right; - line-height: 20px; - max-width: 100%; - } -} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss index 1946cccd319a4d..22f53512be46b1 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/_field_chooser.scss @@ -1,3 +1,8 @@ +.dscFieldChooser { + padding-left: $euiSizeS !important; + padding-right: $euiSizeS !important; +} + .dscFieldChooser__toggle { color: $euiColorMediumShade; margin-left: $euiSizeS !important; @@ -15,3 +20,7 @@ .dscProgressBarTooltip__anchor { display: block; } + +.dscToggleFieldFilterButton { + min-height: $euiSizeXL; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx new file mode 100644 index 00000000000000..cf853d798a8abb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; +import { DiscoverFieldSearch } from './discover_field_search'; + +describe('DiscoverFieldSearch', () => { + function mountComponent() { + const props = { + onChange: jest.fn(), + onShowFilter: jest.fn(), + showFilter: false, + value: 'test', + }; + const comp = mountWithIntl(); + const input = findTestSubject(comp, 'fieldFilterSearchInput'); + const btn = findTestSubject(comp, 'toggleFieldFilterButton'); + return { comp, input, btn, props }; + } + + test('enter value', () => { + const { input, props } = mountComponent(); + input.simulate('change', { target: { value: 'new filter' } }); + expect(props.onChange).toBeCalledTimes(1); + }); + + // this should work, but doesn't, have to do some research + test('click toggle filter button', () => { + const { btn, props } = mountComponent(); + btn.simulate('click'); + expect(props.onShowFilter).toBeCalledTimes(1); + }); + + test('change showFilter value should change button label', () => { + const { btn, comp } = mountComponent(); + const prevFilterBtnHTML = btn.html(); + comp.setProps({ showFilter: true }); + expect(btn.html()).not.toBe(prevFilterBtnHTML); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx new file mode 100644 index 00000000000000..666ccf0acfc7a3 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; + +export interface Props { + /** + * triggered on input of user into search field + */ + onChange: (field: string, value: string) => void; + /** + * triggered when the "additional filter btn" is clicked + */ + onShowFilter: () => void; + /** + * determines whether additional filter fields are displayed + */ + showFilter: boolean; + /** + * the input value of the user + */ + value?: string; +} + +/** + * Component is Discover's side bar to search of available fields + * Additionally there's a button displayed that allows the user to show/hide more filter fields + */ +export function DiscoverFieldSearch({ showFilter, onChange, onShowFilter, value }: Props) { + if (typeof value !== 'string') { + // at initial rendering value is undefined (angular related), this catches the warning + // should be removed once all is react + return null; + } + const filterBtnAriaLabel = showFilter + ? i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel', { + defaultMessage: 'Hide field filter settings', + }) + : i18n.translate('kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel', { + defaultMessage: 'Show field filter settings', + }); + const searchPlaceholder = i18n.translate('kbn.discover.fieldChooser.searchPlaceHolder', { + defaultMessage: 'Search fields', + }); + + return ( + + + onChange('name', event.currentTarget.value)} + placeholder={searchPlaceholder} + value={value} + /> + + + + onShowFilter()} + size="m" + /> + + + + ); +} diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts new file mode 100644 index 00000000000000..baf8f3040d6b02 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search_directive.ts @@ -0,0 +1,33 @@ +/* + * 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. + */ +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { wrapInI18nContext } from 'ui/i18n'; +import { DiscoverFieldSearch } from './discover_field_search'; + +const app = uiModules.get('apps/discover'); + +app.directive('discoverFieldSearch', function(reactDirective: any) { + return reactDirective(wrapInI18nContext(DiscoverFieldSearch), [ + ['onChange', { watchDepth: 'reference' }], + ['onShowFilter', { watchDepth: 'reference' }], + ['showFilter', { watchDepth: 'value' }], + ['value', { watchDepth: 'value' }], + ]); +}); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html index 96aa1582b5243e..d1a75adac5b82f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/field_chooser.html @@ -5,6 +5,75 @@ index-pattern-list="indexPatternList" > + -
    +

-
- -
- - -