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/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.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..2b3b6c899e8ded --- /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: KibanaRequest | LegacyRequest) => 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..45fb697b329f89 --- /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: KibanaRequest<unknown, unknown, unknown> | LegacyRequest) => 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: KibanaRequest<unknown, unknown, unknown> | LegacyRequest, 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..1272a134ef5c44 --- /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: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; +``` 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.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.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..d943228bbea06b 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. | @@ -122,6 +119,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [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) | | +| [IBasePath](./kibana-plugin-server.ibasepath.md) | Access or manipulate the Kibana base path[BasePath](./kibana-plugin-server.basepath.md) | | [IContextHandler](./kibana-plugin-server.icontexthandler.md) | A function registered by a plugin to perform some action. | | [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. | 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/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 ac313331b3152a..8aff95748560db 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@babel/register": "^7.5.5", "@elastic/charts": "^12.0.2", "@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", @@ -379,7 +377,7 @@ "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", @@ -422,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/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/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/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/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..102e77b564a6d6 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -752,7 +752,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 +775,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/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/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/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..d3fe64ddc1e0df 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -55,6 +55,7 @@ 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'; @@ -77,6 +78,8 @@ export { AuthResultParams, AuthStatus, AuthToolkit, + BasePath, + IBasePath, CustomHttpResponseOptions, GetAuthHeaders, GetAuthState, @@ -150,7 +153,7 @@ export { SavedObjectsResolveImportErrorsOptions, SavedObjectsSchema, SavedObjectsSerializer, - SavedObjectsService, + SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from './saved_objects'; @@ -230,9 +233,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/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts b/src/core/server/legacy/plugins/index.ts similarity index 79% rename from src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts rename to src/core/server/legacy/plugins/index.ts index 7354916c3fc359..7c69546f0c4de3 100644 --- a/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts +++ b/src/core/server/legacy/plugins/index.ts @@ -16,13 +16,4 @@ * specific language governing permissions and limitations * under the License. */ - -import { chromeServiceMock } from '../../../../../core/public/mocks'; - -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - chrome: chromeServiceMock.createStartContract(), - }, - }, -})); +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/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx b/src/core/server/saved_objects/saved_objects_config.ts similarity index 65% rename from src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx rename to src/core/server/saved_objects/saved_objects_config.ts index 585fad0e058b73..7217cde55d0611 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.test.mocks.tsx +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -17,16 +17,16 @@ * under the License. */ -import { - fatalErrorsServiceMock, - notificationServiceMock, -} from '../../../../../../../core/public/mocks'; +import { schema, TypeOf } from '@kbn/config-schema'; -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: { - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - notifications: notificationServiceMock.createSetupContract(), - }, - }, -})); +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..ae839644fc2e29 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'; @@ -54,6 +57,17 @@ export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; } +// @public +export class BasePath { + // @internal + constructor(serverBasePath?: string); + get: (request: KibanaRequest | LegacyRequest) => string; + prepend: (path: string) => string; + remove: (path: string) => string; + readonly serverBasePath: string; + set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; +} + // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts // // @internal (undocumented) @@ -230,12 +244,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; @@ -257,6 +266,9 @@ export interface HttpServiceStart { isListening: (port: number) => boolean; } +// @public +export type IBasePath = Pick; + // @public export interface IContextContainer { createHandler(pluginOpaqueId: PluginOpaqueId, handler: IContextHandler): (...rest: THandlerParameters) => THandlerReturn extends Promise ? THandlerReturn : Promise; @@ -290,8 +302,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 +403,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; @@ -824,6 +842,8 @@ export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { defaultSearchOperator?: 'AND' | 'OR'; fields?: string[]; // (undocumented) + filter?: string; + // (undocumented) hasReference?: { type: string; id: string; @@ -946,6 +966,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 +1039,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 +1053,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 +1063,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/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/containers/main/main.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx index d7b369cc264816..82256cf7398820 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { debounce } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; @@ -52,8 +52,6 @@ export function Main() { const [showSettings, setShowSettings] = useState(false); const [showHelp, setShowHelp] = useState(false); - const containerRef = useRef(null); - const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ INITIAL_PANEL_WIDTH, INITIAL_PANEL_WIDTH, @@ -71,9 +69,9 @@ export function Main() { }; return ( -
+ <> setShowSettings(false)} /> : null} {showHelp ? setShowHelp(false)} /> : null} -
+ ); } diff --git a/src/legacy/core_plugins/console/public/quarantined/_app.scss b/src/legacy/core_plugins/console/public/quarantined/_app.scss index 5fd2cd080d06d8..6ec94c8fb4c96f 100644 --- a/src/legacy/core_plugins/console/public/quarantined/_app.scss +++ b/src/legacy/core_plugins/console/public/quarantined/_app.scss @@ -1,6 +1,7 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). #consoleRoot { - height: 100%; + display: flex; + flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container // SASSTODO: Uncomment when tooltips are EUI-ified (inside portals) overflow: hidden; diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index c3618d412f4258..80104fc1991b09 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -45,4 +45,7 @@ export const setup = dataPlugin.setup(npSetup.core, { __LEGACY: legacyPlugin.setup(), }); -export const start = dataPlugin.start(npStart.core); +export const start = dataPlugin.start(npStart.core, { + data: npStart.plugins.data, + __LEGACY: legacyPlugin.start(), +}); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index aec97b02bc2b9f..a5aa55673cac66 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -18,12 +18,16 @@ */ import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { SearchService, SearchSetup } from './search'; +import { SearchService, SearchSetup, createSearchBar, StatetfulSearchBarProps } from './search'; import { QueryService, QuerySetup } from './query'; import { FilterService, FilterSetup } from './filter'; import { TimefilterService, TimefilterSetup } from './timefilter'; import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; -import { LegacyDependenciesPluginSetup } from './shim/legacy_dependencies_plugin'; +import { + LegacyDependenciesPluginSetup, + LegacyDependenciesPluginStart, +} from './shim/legacy_dependencies_plugin'; +import { DataPublicPluginStart } from '../../../../plugins/data/public'; /** * Interface for any dependencies on other plugins' `setup` contracts. @@ -34,6 +38,11 @@ export interface DataPluginSetupDependencies { __LEGACY: LegacyDependenciesPluginSetup; } +export interface DataPluginStartDependencies { + data: DataPublicPluginStart; + __LEGACY: LegacyDependenciesPluginStart; +} + /** * Interface for this plugin's returned `setup` contract. * @@ -47,6 +56,22 @@ export interface DataSetup { timefilter: TimefilterSetup; } +/** + * Interface for this plugin's returned `start` contract. + * + * @public + */ +export interface DataStart { + indexPatterns: IndexPatternsSetup; + filter: FilterSetup; + query: QuerySetup; + search: SearchSetup; + timefilter: TimefilterSetup; + ui: { + SearchBar: React.ComponentType; + }; +} + /** * Data Plugin - public * @@ -58,7 +83,9 @@ export interface DataSetup { * in the setup/start interfaces. The remaining items exported here are either types, * or static code. */ -export class DataPlugin implements Plugin { +export class DataPlugin + implements + Plugin { // Exposed services, sorted alphabetically private readonly filter: FilterService = new FilterService(); private readonly indexPatterns: IndexPatternsService = new IndexPatternsService(); @@ -96,9 +123,20 @@ export class DataPlugin implements Plugin + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-controls="kbnTypeahead__items" + aria-expanded={false} + aria-haspopup="true" + aria-owns="kbnTypeahead__items" + 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-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the test" + autoComplete="off" + autoFocus={false} + 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 - - - - - -
-
- -
-
-
-
-
-
-
- -
-
- +
+ + + +
+ + + + + + + + + + + + + + + + + +
+ `; exports[`QueryBarInput Should pass the query language to the language switcher 1`] = ` - + - -
+ -
-
- + - + -
- - - - -
- - - - + aria-controls="kbnTypeahead__items" + aria-expanded={false} + aria-haspopup="true" + aria-owns="kbnTypeahead__items" + 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-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the test" + 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-controls="kbnTypeahead__items" + aria-expanded={false} + aria-haspopup="true" + aria-owns="kbnTypeahead__items" + 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-activedescendant="" + aria-autocomplete="list" + aria-controls="kbnTypeahead__items" + aria-label="You are on search box of Another Screen page. Start typing to search and filter the test" + 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..6c91da7c28135f 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 @@ -23,19 +23,17 @@ import React from 'react'; 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 +41,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 +57,7 @@ interface Props { languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; onChange?: (query: Query) => void; onSubmit?: (query: Query) => void; + dataTestSubj?: string; } interface State { @@ -107,6 +98,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 +114,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 +129,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 +361,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 +398,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 +411,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,24 +438,6 @@ 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', - ]); - return (
{ }, { previouslyTranslatedPageTitle: this.props.screenTitle, - pageType: this.props.appName, + pageType: this.services.appName, } ) : undefined } type="text" - data-test-subj="queryInput" aria-autocomplete="list" aria-controls="kbnTypeahead__items" aria-activedescendant={ @@ -529,7 +502,7 @@ export class QueryBarInputUI extends Component { onSelectLanguage={this.onSelectLanguage} /> } - {...rest} + data-test-subj={this.props.dataTestSubj || 'queryInput'} />
@@ -548,4 +521,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..ed2a6638aba114 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,52 @@ 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; + savedQuery?: SavedQuery; + onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; onSaved?: (savedQuery: SavedQuery) => void; onSavedQueryUpdated?: (savedQuery: SavedQuery) => void; 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 +99,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 +250,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 +263,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 +282,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 +290,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 +334,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 +367,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 +444,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/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..6eef5047634f4f --- /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..fd80e75b7783f6 --- /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..0ac400dba7bad3 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; 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..434202d64d6c30 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; } 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..34a0d2cd981c5f 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" /> ), 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/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/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/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..2043dc44c147e7 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" > + -
- -
-

- -