diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index ccaa5b11aa80c..c85b4b3b8fe79 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign a PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.1 + uses: elastic/github-actions/project-assigner@v1.0.2 id: project_assigner with: issue-mappings: | diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 737da4f7fe371..efcf53c722527 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -8,7 +8,7 @@ jobs: name: Assign issue or PR to project based on label steps: - name: Assign to project - uses: elastic/github-actions/project-assigner@v1.0.1 + uses: elastic/github-actions/project-assigner@v1.0.2 id: project_assigner with: issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219363}, {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187593}]' diff --git a/.i18nrc.json b/.i18nrc.json index e0acda70cc348..08cf5a2823203 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -33,6 +33,7 @@ "navigation": "src/plugins/navigation", "newsfeed": "src/plugins/newsfeed", "regionMap": "src/legacy/core_plugins/region_map", + "savedObjects": "src/plugins/saved_objects", "server": "src/legacy/server", "statusPage": "src/legacy/core_plugins/status_page", "telemetry": "src/legacy/core_plugins/telemetry", diff --git a/.node-version b/.node-version index 06c9b9d306348..5b7269c0a98f3 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -10.18.0 +10.19.0 diff --git a/.nvmrc b/.nvmrc index 06c9b9d306348..5b7269c0a98f3 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -10.18.0 +10.19.0 diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index a3abeff44c25c..482f014b226b9 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -25,6 +25,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [SavedObjectsRepository](./kibana-plugin-server.savedobjectsrepository.md) | | +| [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) | A serializer that can be used to manually convert [raw](./kibana-plugin-server.savedobjectsrawdoc.md) or [sanitized](./kibana-plugin-server.savedobjectsanitizeddoc.md) documents to the other kind. | | [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.See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md). | ## Enumerations @@ -106,6 +107,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) | Additional options for the RouteValidator class to modify its default behaviour. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | | [SavedObjectAttributes](./kibana-plugin-server.savedobjectattributes.md) | The data for a Saved Object is stored as an object in the attributes property. | +| [SavedObjectMigrationMap](./kibana-plugin-server.savedobjectmigrationmap.md) | A map of [migration functions](./kibana-plugin-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions.For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. | | [SavedObjectReference](./kibana-plugin-server.savedobjectreference.md) | A reference to another saved object. | | [SavedObjectsBaseOptions](./kibana-plugin-server.savedobjectsbaseoptions.md) | | | [SavedObjectsBulkCreateObject](./kibana-plugin-server.savedobjectsbulkcreateobject.md) | | @@ -116,6 +118,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsBulkUpdateResponse](./kibana-plugin-server.savedobjectsbulkupdateresponse.md) | | | [SavedObjectsClientProviderOptions](./kibana-plugin-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | +| [SavedObjectsComplexFieldMapping](./kibana-plugin-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-server.savedobjectsfieldmapping.md) for documentation. | +| [SavedObjectsCoreFieldMapping](./kibana-plugin-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-server.savedobjectsdeletebynamespaceoptions.md) | | | [SavedObjectsDeleteOptions](./kibana-plugin-server.savedobjectsdeleteoptions.md) | | @@ -132,6 +136,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsImportUnknownError](./kibana-plugin-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | | [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | | [SavedObjectsIncrementCounterOptions](./kibana-plugin-server.savedobjectsincrementcounteroptions.md) | | +| [SavedObjectsMappingProperties](./kibana-plugin-server.savedobjectsmappingproperties.md) | Describe the fields of a [saved object type](./kibana-plugin-server.savedobjectstypemappingdefinition.md). | | [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) | | | [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. | @@ -139,6 +144,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsResolveImportErrorsOptions](./kibana-plugin-server.savedobjectsresolveimporterrorsoptions.md) | Options to control the "resolve import" operation. | | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | Saved Objects is Kibana's data persistence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceSetup API exposes methods for creating and registering Saved Object client wrappers. | | [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing and querying state. The SavedObjectsServiceStart API provides a scoped Saved Objects client for interacting with Saved Objects. | +| [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) | | +| [SavedObjectsTypeMappingDefinition](./kibana-plugin-server.savedobjectstypemappingdefinition.md) | Describe a saved object type mapping. | | [SavedObjectsUpdateOptions](./kibana-plugin-server.savedobjectsupdateoptions.md) | | | [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) | | | [SessionCookieValidationResult](./kibana-plugin-server.sessioncookievalidationresult.md) | Return type from a function to validate cookie contents. | @@ -218,10 +225,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteValidatorFullConfig](./kibana-plugin-server.routevalidatorfullconfig.md) | Route validations config and options merged into one object | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | +| [SavedObjectMigrationFn](./kibana-plugin-server.savedobjectmigrationfn.md) | A migration function defined for a [saved objects type](./kibana-plugin-server.savedobjectstype.md) used to migrate it's | +| [SavedObjectSanitizedDoc](./kibana-plugin-server.savedobjectsanitizeddoc.md) | | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientFactoryProvider](./kibana-plugin-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | +| [SavedObjectsFieldMapping](./kibana-plugin-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | | [ScopeableRequest](./kibana-plugin-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-server.kibanarequest.md). | | [SharedGlobalConfig](./kibana-plugin-server.sharedglobalconfig.md) | | | [StringValidation](./kibana-plugin-server.stringvalidation.md) | Allows regex objects or a regex string | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectmigrationfn.md b/docs/development/core/server/kibana-plugin-server.savedobjectmigrationfn.md new file mode 100644 index 0000000000000..629d748083737 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectmigrationfn.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectMigrationFn](./kibana-plugin-server.savedobjectmigrationfn.md) + +## SavedObjectMigrationFn type + +A migration function defined for a [saved objects type](./kibana-plugin-server.savedobjectstype.md) used to migrate it's + +Signature: + +```typescript +export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, log: SavedObjectsMigrationLogger) => SavedObjectUnsanitizedDoc; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectmigrationmap.md b/docs/development/core/server/kibana-plugin-server.savedobjectmigrationmap.md new file mode 100644 index 0000000000000..1f49b44f73a72 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectmigrationmap.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectMigrationMap](./kibana-plugin-server.savedobjectmigrationmap.md) + +## SavedObjectMigrationMap interface + +A map of [migration functions](./kibana-plugin-server.savedobjectmigrationfn.md) to be used for a given type. The map's keys must be valid semver versions. + +For a given document, only migrations with a higher version number than that of the document will be applied. Migrations are executed in order, starting from the lowest version and ending with the highest one. + +Signature: + +```typescript +export interface SavedObjectMigrationMap +``` + +## Example + + +```typescript +const migrations: SavedObjectMigrationMap = { + '1.0.0': migrateToV1, + '2.1.0': migrateToV21 +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsanitizeddoc.md b/docs/development/core/server/kibana-plugin-server.savedobjectsanitizeddoc.md new file mode 100644 index 0000000000000..3c7cd591cd41b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsanitizeddoc.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectSanitizedDoc](./kibana-plugin-server.savedobjectsanitizeddoc.md) + +## SavedObjectSanitizedDoc type + + +Signature: + +```typescript +export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.dynamic.md b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.dynamic.md new file mode 100644 index 0000000000000..44e548c611b02 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.dynamic.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-server.savedobjectscomplexfieldmapping.md) > [dynamic](./kibana-plugin-server.savedobjectscomplexfieldmapping.dynamic.md) + +## SavedObjectsComplexFieldMapping.dynamic property + +Signature: + +```typescript +dynamic?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.md new file mode 100644 index 0000000000000..ecb5ec0e88911 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-server.savedobjectscomplexfieldmapping.md) + +## SavedObjectsComplexFieldMapping interface + +See [SavedObjectsFieldMapping](./kibana-plugin-server.savedobjectsfieldmapping.md) for documentation. + +Signature: + +```typescript +export interface SavedObjectsComplexFieldMapping +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [dynamic](./kibana-plugin-server.savedobjectscomplexfieldmapping.dynamic.md) | string | | +| [properties](./kibana-plugin-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties | | +| [type](./kibana-plugin-server.savedobjectscomplexfieldmapping.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.properties.md b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.properties.md new file mode 100644 index 0000000000000..7ceb0f8ee43c5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.properties.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-server.savedobjectscomplexfieldmapping.md) > [properties](./kibana-plugin-server.savedobjectscomplexfieldmapping.properties.md) + +## SavedObjectsComplexFieldMapping.properties property + +Signature: + +```typescript +properties: SavedObjectsMappingProperties; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.type.md new file mode 100644 index 0000000000000..ef0db5123c927 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscomplexfieldmapping.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-server.savedobjectscomplexfieldmapping.md) > [type](./kibana-plugin-server.savedobjectscomplexfieldmapping.type.md) + +## SavedObjectsComplexFieldMapping.type property + +Signature: + +```typescript +type?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.enabled.md b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.enabled.md new file mode 100644 index 0000000000000..b7e4203977763 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.enabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-server.savedobjectscorefieldmapping.md) > [enabled](./kibana-plugin-server.savedobjectscorefieldmapping.enabled.md) + +## SavedObjectsCoreFieldMapping.enabled property + +Signature: + +```typescript +enabled?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.fields.md b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.fields.md new file mode 100644 index 0000000000000..880ef36cd73fc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.fields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-server.savedobjectscorefieldmapping.md) > [fields](./kibana-plugin-server.savedobjectscorefieldmapping.fields.md) + +## SavedObjectsCoreFieldMapping.fields property + +Signature: + +```typescript +fields?: { + [subfield: string]: { + type: string; + }; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.index.md b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.index.md new file mode 100644 index 0000000000000..31ff5a1d51948 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.index.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-server.savedobjectscorefieldmapping.md) > [index](./kibana-plugin-server.savedobjectscorefieldmapping.index.md) + +## SavedObjectsCoreFieldMapping.index property + +Signature: + +```typescript +index?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.md new file mode 100644 index 0000000000000..4287921997098 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-server.savedobjectscorefieldmapping.md) + +## SavedObjectsCoreFieldMapping interface + +See [SavedObjectsFieldMapping](./kibana-plugin-server.savedobjectsfieldmapping.md) for documentation. + +Signature: + +```typescript +export interface SavedObjectsCoreFieldMapping +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [enabled](./kibana-plugin-server.savedobjectscorefieldmapping.enabled.md) | boolean | | +| [fields](./kibana-plugin-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
};
} | | +| [index](./kibana-plugin-server.savedobjectscorefieldmapping.index.md) | boolean | | +| [type](./kibana-plugin-server.savedobjectscorefieldmapping.type.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.type.md b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.type.md new file mode 100644 index 0000000000000..8071217af861f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectscorefieldmapping.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-server.savedobjectscorefieldmapping.md) > [type](./kibana-plugin-server.savedobjectscorefieldmapping.type.md) + +## SavedObjectsCoreFieldMapping.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsfieldmapping.md b/docs/development/core/server/kibana-plugin-server.savedobjectsfieldmapping.md new file mode 100644 index 0000000000000..f31e18e8df670 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsfieldmapping.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsFieldMapping](./kibana-plugin-server.savedobjectsfieldmapping.md) + +## SavedObjectsFieldMapping type + +Describe a [saved object type mapping](./kibana-plugin-server.savedobjectstypemappingdefinition.md) field. + +Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation + +Signature: + +```typescript +export declare type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjectsComplexFieldMapping; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmappingproperties.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmappingproperties.md new file mode 100644 index 0000000000000..23c5a83afc1f8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmappingproperties.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMappingProperties](./kibana-plugin-server.savedobjectsmappingproperties.md) + +## SavedObjectsMappingProperties interface + +Describe the fields of a [saved object type](./kibana-plugin-server.savedobjectstypemappingdefinition.md). + +Signature: + +```typescript +export interface SavedObjectsMappingProperties +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md index a98d88700cd55..1b8db4a6c9f8d 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.md @@ -17,5 +17,6 @@ export interface SavedObjectsMigrationLogger | --- | --- | --- | | [debug](./kibana-plugin-server.savedobjectsmigrationlogger.debug.md) | (msg: string) => void | | | [info](./kibana-plugin-server.savedobjectsmigrationlogger.info.md) | (msg: string) => void | | +| [warn](./kibana-plugin-server.savedobjectsmigrationlogger.warn.md) | (msg: string) => void | | | [warning](./kibana-plugin-server.savedobjectsmigrationlogger.warning.md) | (msg: string) => void | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warn.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warn.md new file mode 100644 index 0000000000000..7ebb1dd5ee4d2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warn.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsMigrationLogger](./kibana-plugin-server.savedobjectsmigrationlogger.md) > [warn](./kibana-plugin-server.savedobjectsmigrationlogger.warn.md) + +## SavedObjectsMigrationLogger.warn property + +Signature: + +```typescript +warn: (msg: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md index a87955d603b70..689a799515094 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsmigrationlogger.warning.md @@ -4,6 +4,11 @@ ## SavedObjectsMigrationLogger.warning property +> Warning: This API is now obsolete. +> +> Use `warn` instead. +> + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._source.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._source.md index dcf207f8120ea..332df982b23eb 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._source.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc._source.md @@ -7,5 +7,5 @@ Signature: ```typescript -_source: any; +_source: SavedObjectsRawDocSource; ``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc.md b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc.md index b0130df01817f..3e3ad7da5aecd 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsrawdoc.md @@ -9,7 +9,7 @@ A raw document as represented directly in the saved object index. Signature: ```typescript -export interface RawDoc +export interface SavedObjectsRawDoc ``` ## Properties @@ -19,6 +19,6 @@ export interface RawDoc | [\_id](./kibana-plugin-server.savedobjectsrawdoc._id.md) | string | | | [\_primary\_term](./kibana-plugin-server.savedobjectsrawdoc._primary_term.md) | number | | | [\_seq\_no](./kibana-plugin-server.savedobjectsrawdoc._seq_no.md) | number | | -| [\_source](./kibana-plugin-server.savedobjectsrawdoc._source.md) | any | | +| [\_source](./kibana-plugin-server.savedobjectsrawdoc._source.md) | SavedObjectsRawDocSource | | | [\_type](./kibana-plugin-server.savedobjectsrawdoc._type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md new file mode 100644 index 0000000000000..50fb4433b33c8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.generaterawid.md @@ -0,0 +1,26 @@ + + +[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 new file mode 100644 index 0000000000000..32d00ee1a9f7f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.israwsavedobject.md @@ -0,0 +1,24 @@ + + +[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: SavedObjectsRawDoc): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| rawDoc | SavedObjectsRawDoc | | + +Returns: + +`boolean` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md new file mode 100644 index 0000000000000..5fa84af147b40 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) + +## SavedObjectsSerializer class + +A serializer that can be used to manually convert [raw](./kibana-plugin-server.savedobjectsrawdoc.md) or [sanitized](./kibana-plugin-server.savedobjectsanitizeddoc.md) documents to the other kind. + +Signature: + +```typescript +export declare class SavedObjectsSerializer +``` + +## Remarks + +Serializer instances should only be created and accessed by calling [SavedObjectsServiceStart.createSerializer](./kibana-plugin-server.savedobjectsservicestart.createserializer.md) + +The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend 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 new file mode 100644 index 0000000000000..6a8a006241297 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.rawtosavedobject.md @@ -0,0 +1,24 @@ + + +[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: SavedObjectsRawDoc): SavedObjectSanitizedDoc; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| doc | SavedObjectsRawDoc | | + +Returns: + +`SavedObjectSanitizedDoc` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md new file mode 100644 index 0000000000000..b0d2043665b54 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsserializer.savedobjecttoraw.md @@ -0,0 +1,24 @@ + + +[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: SavedObjectSanitizedDoc): SavedObjectsRawDoc; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| savedObj | SavedObjectSanitizedDoc | | + +Returns: + +`SavedObjectsRawDoc` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md index 64fb1f4a5f638..9981bfee0cb7d 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.md @@ -20,9 +20,19 @@ When plugins access the Saved Objects client, a new client is created using the ## Example + +```ts import { SavedObjectsClient, CoreSetup } from 'src/core/server'; -export class Plugin() { setup: (core: CoreSetup) => { core.savedObjects.setClientFactory(({ request: KibanaRequest }) => { return new SavedObjectsClient(core.savedObjects.scopedRepository(request)); }) } } +export class Plugin() { + setup: (core: CoreSetup) => { + core.savedObjects.setClientFactory(({ request: KibanaRequest }) => { + return new SavedObjectsClient(core.savedObjects.scopedRepository(request)); + }) + } +} + +``` ## Properties diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.createserializer.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.createserializer.md new file mode 100644 index 0000000000000..b0e7ae1db89c1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.createserializer.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) > [createSerializer](./kibana-plugin-server.savedobjectsservicestart.createserializer.md) + +## SavedObjectsServiceStart.createSerializer property + +Creates a [serializer](./kibana-plugin-server.savedobjectsserializer.md) that is aware of all registered types. + +Signature: + +```typescript +createSerializer: () => SavedObjectsSerializer; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md index cf2b4f57a7461..ad34d76bb33f4 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.md @@ -18,5 +18,6 @@ export interface SavedObjectsServiceStart | --- | --- | --- | | [createInternalRepository](./kibana-plugin-server.savedobjectsservicestart.createinternalrepository.md) | (extraTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-server.isavedobjectsrepository.md) that uses the internal Kibana user for authenticating with Elasticsearch. | | [createScopedRepository](./kibana-plugin-server.savedobjectsservicestart.createscopedrepository.md) | (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository | Creates a [Saved Objects repository](./kibana-plugin-server.isavedobjectsrepository.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. | +| [createSerializer](./kibana-plugin-server.savedobjectsservicestart.createserializer.md) | () => SavedObjectsSerializer | Creates a [serializer](./kibana-plugin-server.savedobjectsserializer.md) that is aware of all registered types. | | [getScopedClient](./kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | (req: KibanaRequest, options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract | Creates a [Saved Objects client](./kibana-plugin-server.savedobjectsclientcontract.md) that uses the credentials from the passed in request to authenticate with Elasticsearch. If other plugins have registered Saved Objects client wrappers, these will be applied to extend the functionality of the client.A client that is already scoped to the incoming request is also exposed from the route handler context see [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md). | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.converttoaliasscript.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.converttoaliasscript.md new file mode 100644 index 0000000000000..f2519b2600e2f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.converttoaliasscript.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) > [convertToAliasScript](./kibana-plugin-server.savedobjectstype.converttoaliasscript.md) + +## SavedObjectsType.convertToAliasScript property + +If defined, will be used to convert the type to an alias. + +Signature: + +```typescript +convertToAliasScript?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.hidden.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.hidden.md new file mode 100644 index 0000000000000..9e611bc8faf27 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.hidden.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) > [hidden](./kibana-plugin-server.savedobjectstype.hidden.md) + +## SavedObjectsType.hidden property + +Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an `extraType` when creating the repository. + +See [createInternalRepository](./kibana-plugin-server.savedobjectsservicestart.createinternalrepository.md). + +Signature: + +```typescript +hidden: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.indexpattern.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.indexpattern.md new file mode 100644 index 0000000000000..18cb6fe936060 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.indexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) > [indexPattern](./kibana-plugin-server.savedobjectstype.indexpattern.md) + +## SavedObjectsType.indexPattern property + +If defined, the type instances will be stored in the given index instead of the default one. + +Signature: + +```typescript +indexPattern?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.mappings.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.mappings.md new file mode 100644 index 0000000000000..3166a2604d728 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.mappings.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) > [mappings](./kibana-plugin-server.savedobjectstype.mappings.md) + +## SavedObjectsType.mappings property + +The [mapping definition](./kibana-plugin-server.savedobjectstypemappingdefinition.md) for the type. + +Signature: + +```typescript +mappings: SavedObjectsTypeMappingDefinition; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.md new file mode 100644 index 0000000000000..1e989652e52bf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) + +## SavedObjectsType interface + +Signature: + +```typescript +export interface SavedObjectsType +``` + +## Remarks + +This is only internal for now, and will only be public when we expose the registerType API + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [convertToAliasScript](./kibana-plugin-server.savedobjectstype.converttoaliasscript.md) | string | If defined, will be used to convert the type to an alias. | +| [hidden](./kibana-plugin-server.savedobjectstype.hidden.md) | boolean | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an extraType when creating the repository.See [createInternalRepository](./kibana-plugin-server.savedobjectsservicestart.createinternalrepository.md). | +| [indexPattern](./kibana-plugin-server.savedobjectstype.indexpattern.md) | string | If defined, the type instances will be stored in the given index instead of the default one. | +| [mappings](./kibana-plugin-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-server.savedobjectstypemappingdefinition.md) for the type. | +| [migrations](./kibana-plugin-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | An optional map of [migrations](./kibana-plugin-server.savedobjectmigrationfn.md) to be used to migrate the type. | +| [name](./kibana-plugin-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | +| [namespaceAgnostic](./kibana-plugin-server.savedobjectstype.namespaceagnostic.md) | boolean | Is the type global (true), or namespaced (false). | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.migrations.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.migrations.md new file mode 100644 index 0000000000000..a7f933ef6e3f7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.migrations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) > [migrations](./kibana-plugin-server.savedobjectstype.migrations.md) + +## SavedObjectsType.migrations property + +An optional map of [migrations](./kibana-plugin-server.savedobjectmigrationfn.md) to be used to migrate the type. + +Signature: + +```typescript +migrations?: SavedObjectMigrationMap; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.name.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.name.md new file mode 100644 index 0000000000000..444152d239cdf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.name.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) > [name](./kibana-plugin-server.savedobjectstype.name.md) + +## SavedObjectsType.name property + +The name of the type, which is also used as the internal id. + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstype.namespaceagnostic.md b/docs/development/core/server/kibana-plugin-server.savedobjectstype.namespaceagnostic.md new file mode 100644 index 0000000000000..5c68942276ec7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstype.namespaceagnostic.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsType](./kibana-plugin-server.savedobjectstype.md) > [namespaceAgnostic](./kibana-plugin-server.savedobjectstype.namespaceagnostic.md) + +## SavedObjectsType.namespaceAgnostic property + +Is the type global (true), or namespaced (false). + +Signature: + +```typescript +namespaceAgnostic: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstypemappingdefinition.md b/docs/development/core/server/kibana-plugin-server.savedobjectstypemappingdefinition.md new file mode 100644 index 0000000000000..99983d3a9f02b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstypemappingdefinition.md @@ -0,0 +1,45 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsTypeMappingDefinition](./kibana-plugin-server.savedobjectstypemappingdefinition.md) + +## SavedObjectsTypeMappingDefinition interface + +Describe a saved object type mapping. + +Signature: + +```typescript +export interface SavedObjectsTypeMappingDefinition +``` + +## Example + + +```ts +const typeDefinition: SavedObjectsTypeMappingDefinition = { + properties: { + enabled: { + type: "boolean" + }, + sendUsageFrom: { + ignore_above: 256, + type: "keyword" + }, + lastReported: { + type: "date" + }, + lastVersionChecked: { + ignore_above: 256, + type: "keyword" + }, + } +} + +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [properties](./kibana-plugin-server.savedobjectstypemappingdefinition.properties.md) | SavedObjectsMappingProperties | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectstypemappingdefinition.properties.md b/docs/development/core/server/kibana-plugin-server.savedobjectstypemappingdefinition.properties.md new file mode 100644 index 0000000000000..555870c3fdd7d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectstypemappingdefinition.properties.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsTypeMappingDefinition](./kibana-plugin-server.savedobjectstypemappingdefinition.md) > [properties](./kibana-plugin-server.savedobjectstypemappingdefinition.properties.md) + +## SavedObjectsTypeMappingDefinition.properties property + +Signature: + +```typescript +properties: SavedObjectsMappingProperties; +``` diff --git a/docs/images/management-index-patterns.png b/docs/images/management-index-patterns.png new file mode 100644 index 0000000000000..232d32893b96d Binary files /dev/null and b/docs/images/management-index-patterns.png differ diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 695a4d4f45b02..d4d766c1f5442 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -1,7 +1,7 @@ [[advanced-options]] -== Setting advanced options +== Advanced Settings -The *Advanced Settings* page enables you to directly edit settings that control the behavior of the Kibana application. +The *Advanced Settings* UI enables you to edit settings that control the behavior of Kibana. For example, you can change the format used to display dates, specify the default index pattern, and set the precision for displayed decimal values. diff --git a/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc b/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc index 79bd0b8be3ce9..ba1d79710de05 100644 --- a/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc +++ b/docs/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc @@ -1,30 +1,30 @@ [role="xpack"] [[index-lifecycle-policies]] -== Index lifecycle policies +== Index Lifecycle Policies -If you're working with time series data, you don't want to continually dump -everything into a single index. Instead, you might periodically roll over the -data to a new index to keep it from growing so big it's slow and expensive. -As the index ages and you query it less frequently, you’ll likely move it to +If you're working with time series data, you don't want to continually dump +everything into a single index. Instead, you might periodically roll over the +data to a new index to keep it from growing so big it's slow and expensive. +As the index ages and you query it less frequently, you’ll likely move it to less expensive hardware and reduce the number of shards and replicas. -To automatically move an index through its lifecycle, you can create a policy -to define actions to perform on the index as it ages. Index lifecycle policies -are especially useful when working with {beats-ref}/beats-reference.html[Beats] -data shippers, which continually -send operational data, such as metrics and logs, to Elasticsearch. You can -automate a rollover to a new index when the existing index reaches a specified -size or age. This ensures that all indices have a similar size instead of having -daily indices where size can vary based on the number of Beats and the number +To automatically move an index through its lifecycle, you can create a policy +to define actions to perform on the index as it ages. Index lifecycle policies +are especially useful when working with {beats-ref}/beats-reference.html[Beats] +data shippers, which continually +send operational data, such as metrics and logs, to Elasticsearch. You can +automate a rollover to a new index when the existing index reaches a specified +size or age. This ensures that all indices have a similar size instead of having +daily indices where size can vary based on the number of Beats and the number of events sent. -{kib}’s *Index Lifecycle Policies* walks you through the process for creating -and configuring a policy. Before using this feature, you should be familiar +{kib}’s *Index Lifecycle Policies* walks you through the process for creating +and configuring a policy. Before using this feature, you should be familiar with index lifecycle management: -* For an introduction, see -{ref}/getting-started-index-lifecycle-management.html[Getting started with index -lifecycle management]. -* To dig into the concepts and technical details, see +* For an introduction, refer to +{ref}/getting-started-index-lifecycle-management.html[Getting started with index +lifecycle management]. +* To dig into the concepts and technical details, see {ref}/index-lifecycle-management.html[Managing the index lifecycle]. * To check out the APIs, see {ref}/index-lifecycle-management-api.html[Index lifecycle management API]. diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 8e687f641c92b..d8073e4590c3c 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -89,39 +89,11 @@ pattern: `*:logstash-*`. Once an index pattern is configured using the {ccs} syntax, all searches and aggregations using that index pattern in {kib} take advantage of {ccs}. + [float] +[[reload-fields]] === Manage your index pattern -Once you create an index pattern, manually or with a sample data set, -you can look at its fields and associated data types. -You can also perform housekeeping tasks, such as making the -index pattern the default or deleting it when you longer need it. -To drill down into the details of an index pattern, click its name in -the *Index patterns* overview. - -[role="screenshot"] -image:management/index-patterns/images/new-index-pattern.png["Index files and data types"] - -From the detailed view, you can perform the following actions: - -* *Manage the index fields.* You can add formatters to format values and create -scripted fields. -See <> for more information. - -* [[set-default-pattern]]*Set the default index pattern.* {kib} uses a badge to make users -aware of which index pattern is the default. The first pattern -you create is automatically designated as the default pattern. The default -index pattern is loaded when you open *Discover*. - -* [[reload-fields]]*Refresh the index fields list.* You can refresh the index fields list to -pick up any newly-added fields. Doing so also resets Kibana’s popularity counters -for the fields. The popularity counters are used in *Discover* to sort fields in lists. - -* [[delete-pattern]]*Delete the index pattern.* This action removes the pattern from the list of -Saved Objects in {kib}. You will not be able to recover field formatters, -scripted fields, source filters, and field popularity data associated with the index pattern. -Deleting an index pattern does -not remove any indices or data documents from {es}. -+ -WARNING: Deleting an index pattern breaks all visualizations, saved searches, and -other saved objects that reference the pattern. +To drill down into the fields and associated data types in an index pattern, +click its name in the *Index patterns* overview page. +For more information, refer to <>. diff --git a/docs/management/index-patterns/images/edit_icon.png b/docs/management/index-patterns/images/edit_icon.png new file mode 100644 index 0000000000000..d5af1751809cc Binary files /dev/null and b/docs/management/index-patterns/images/edit_icon.png differ diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc index 13e8f52f29b87..5a23b60307131 100644 --- a/docs/management/managing-beats.asciidoc +++ b/docs/management/managing-beats.asciidoc @@ -1,6 +1,6 @@ [[managing-beats]] [role="xpack"] -== Managing {beats} +== {beats} Central Management include::{asciidoc-dir}/../../shared/discontinued.asciidoc[tag=cm-discontinued] @@ -34,14 +34,14 @@ Central Management UI. You need to enroll {beats} to register them in central management and establish trust. Enrolled {beats} will have the credentials needed to retrieve -configurations from {kib}. +configurations from {kib}. [float] === Create configuration tags A _configuration tag_ is a group of configuration blocks that you can apply to one or more {beats}. For example, you can create a tag called `development` to -group configurations for {beats} running in your development environment. +group configurations for {beats} running in your development environment. The first time you walk through the enrollment process, you'll create a configuration tag that's applied to the {beats} instance you're enrolling. @@ -62,7 +62,7 @@ Central management supports configuration settings for: * {filebeat} modules * {metricbeat} modules * {filebeat} inputs -* {filebeat} and {metricbeat} outputs +* {filebeat} and {metricbeat} outputs NOTE: Central management supports the following outputs only: {es}, {ls}, Kafka, and Redis. Other output types are not supported for {beats} that are enrolled in @@ -72,7 +72,7 @@ Use the Central Management UI to define and manage settings for supported configuration blocks. You cannot define those settings in local {beats} configuration files. For configuration blocks that are not supported by central management, configure the settings in the local configuration file after -enrolling the Beat in central management. +enrolling the Beat in central management. [float] === Manage enrolled {beats} diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 308e61abf70e5..f66976b3715d1 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -1,12 +1,56 @@ [[managing-fields]] -== Managing Fields +== Index Patterns and Fields -The fields for the index pattern are listed in a table. Click a column header to sort the table by that column. Click -the *Controls* button in the rightmost column for a given field to edit the field's properties. You can manually set -the field's format from the *Format* drop-down. Format options vary based on the field's type. +The *Index patterns* UI helps you create and manage +the index patterns that retrieve your data from Elasticsearch. -You can also set the field's popularity value in the *Popularity* text entry box to any desired value. Click the -*Update Field* button to confirm your changes or *Cancel* to return to the list of fields. +[role="screenshot"] +image::images/management-index-patterns.png[] + +[float] +=== Create an index pattern + +An index pattern is the glue that connects Kibana to your Elasticsearch data. Create an +index pattern whenever you load your own data into Kibana. To get started, +click *Create index pattern*, and then follow the guided steps. Refer to +<> for the types of index patterns +that you can create. + +[float] +=== Manage your index pattern + +To view the fields and associated data types in an index pattern, click its name in +the *Index patterns* overview. + +[role="screenshot"] +image::management/index-patterns/images/new-index-pattern.png["Index files and data types"] + +Use the icons in the upper right to perform the following actions: + +* [[set-default-pattern]]*Set the default index pattern.* {kib} uses a badge to make users +aware of which index pattern is the default. The first pattern +you create is automatically designated as the default pattern. The default +index pattern is loaded when you open *Discover*. + +* *Refresh the index fields list.* You can refresh the index fields list to +pick up any newly-added fields. Doing so also resets Kibana’s popularity counters +for the fields. The popularity counters are used in *Discover* to sort fields in lists. + +* [[delete-pattern]]*Delete the index pattern.* This action removes the pattern from the list of +Saved Objects in {kib}. You will not be able to recover field formatters, +scripted fields, source filters, and field popularity data associated with the index pattern. +Deleting an index pattern does +not remove any indices or data documents from {es}. ++ +WARNING: Deleting an index pattern breaks all visualizations, saved searches, and +other saved objects that reference the pattern. + +[float] +=== Edit a field + +To edit a field's properties, click the edit icon +image:management/index-patterns/images/edit_icon.png[] in the detail view. +You can set the field's format and popularity value. Kibana has field formatters for the following field types: diff --git a/docs/management/managing-indices.asciidoc b/docs/management/managing-indices.asciidoc index 7a3480c860b16..933a2ffbf6ee2 100644 --- a/docs/management/managing-indices.asciidoc +++ b/docs/management/managing-indices.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[managing-indices]] -== Index management +== Index Management *Index Management* enables you to view index settings, mappings, and statistics and perform index-level operations. @@ -127,7 +127,7 @@ under the *Mapped fields* tab as follows: image::images/management-index-templates-mappings.png[Mapped fields page] You can create additional mapping configurations in the *Dynamic templates* and -*Advanced options* tabs. No additional mappings are required for this example. +*Advanced options* tabs. No additional mappings are required for this example. In the fourth step, define an alias named `logstash`. diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index ecb550d3ab267..72accdb5fe2aa 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -1,32 +1,32 @@ [[managing-licenses]] -== License management +== License Management When you install the default distribution of {kib}, you receive a basic license with no expiration date. For the full list of free features that are included in -the basic license, see https://www.elastic.co/subscriptions[the subscription page]. +the basic license, refer to https://www.elastic.co/subscriptions[the subscription page]. If you want to try out the full set of platinum features, you can activate a -30-day trial license. Go to *Management > License Management* to view the +30-day trial license. Go to *Management > License Management* to view the status of your license, start a trial, or install a new license. NOTE: You can start a trial only if your cluster has not already activated a trial license for the current major product version. For example, if you have -already activated a trial for v6.0, you cannot start a new trial until -v7.0. You can, however, contact `info@elastic.co` to request an extended trial +already activated a trial for 6.0, you cannot start a new trial until +7.0. You can, however, contact `info@elastic.co` to request an extended trial license. -When you activate a new license level, new features appear in the left sidebar +When you activate a new license level, new features appear in the left sidebar of the *Management* page. [role="screenshot"] image::images/management-license.png[] At the end of the trial period, the platinum features operate in a -<>. You can revert to a basic license, -extend the trial, or purchase a subscription. +<>. You can revert to a basic license, +extend the trial, or purchase a subscription. -TIP: If {security-features} are enabled, unless you have a trial license, -you must configure Transport Layer Security (TLS) in {es}. +TIP: If {security-features} are enabled, unless you have a trial license, +you must configure Transport Layer Security (TLS) in {es}. See {ref}/encrypting-communications.html[Encrypting communications]. {kib} and the {ref}/start-basic.html[start basic API] provide a list of all of the features that will no longer be supported if you revert to a basic license. @@ -42,7 +42,7 @@ file that you install in {kib} or by using the TIP: If you are using a basic or trial license, {security-features} are disabled by default. In all other licenses, {security-features} are enabled by default; -you must secure the {stack} or disable the {security-features}. +you must secure the {stack} or disable the {security-features}. [discrete] [[license-expiration]] @@ -97,7 +97,7 @@ cluster. and start {dfeeds} are disabled. * All started {dfeeds} are stopped. * All open {anomaly-jobs} are closed. -* APIs to create and start {dfanalytics-jobs} are disabled. +* APIs to create and start {dfanalytics-jobs} are disabled. * Existing {anomaly-job} and {dfanalytics-job} results continue to be available by using {kib} or APIs. diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc index a776cdf0334cb..6b69cfef5b768 100644 --- a/docs/management/managing-remote-clusters.asciidoc +++ b/docs/management/managing-remote-clusters.asciidoc @@ -1,8 +1,8 @@ [[working-remote-clusters]] -== Working with remote clusters +== Remote Clusters -{kib} *Management* provides user interfaces for working with data from remote -clusters and managing the {ccr} process. You can replicate indices from a +{kib} *Management* provides user interfaces for working with data from remote +clusters and managing the {ccr} process. You can replicate indices from a leader remote cluster to a follower index in a local cluster. The local follower indices can be used to provide remote backups for disaster recovery or for geo-proximite copies of data. @@ -14,51 +14,51 @@ Before using these features, you should be familiar with the following concepts: [float] [[managing-remote-clusters]] -== Managing remote clusters +== Managing remote clusters -*Remote clusters* helps you manage remote clusters for use with -{ccs} and {ccr}. You can add and remove remote clusters and check their connectivity. +*Remote clusters* helps you manage remote clusters for use with +{ccs} and {ccr}. You can add and remove remote clusters and check their connectivity. + +Before you use this feature, you should be familiar with the concept of +{ref}/modules-remote-clusters.html[remote clusters]. -Before you use this feature, you should be familiar with the concept of -{ref}/modules-remote-clusters.html[remote clusters]. - Go to *Management > Elasticsearch > Remote clusters* to create or manage your remotes. -To set up a new remote, click *Add a remote cluster*. Give the cluster a unique name -and define the seed nodes for cluster discovery. You can edit or remove your remote clusters +To set up a new remote, click *Add a remote cluster*. Give the cluster a unique name +and define the seed nodes for cluster discovery. You can edit or remove your remote clusters from the *Remote clusters* list view. [role="screenshot"] image::images/add_remote_cluster.png[][UI for adding a remote cluster] -Once a remote cluster is registered, you can use the tools under *{ccr-cap}* -to add and manage follower indices on the local cluster, and replicate data from +Once a remote cluster is registered, you can use the tools under *{ccr-cap}* +to add and manage follower indices on the local cluster, and replicate data from indices on the remote cluster based on an auto-follow index pattern. [float] [[managing-cross-cluster-replication]] == [xpack]#Managing {ccr}# -*{ccr-cap}* helps you create and manage the {ccr} process. -If you want to replicate data from existing indices, or set up -local followers on a case-by-case basis, go to *Follower indices*. -If you want to automatically detect and follow new indices when they are created -on a remote cluster, you can do so from *Auto-follow patterns*. +*{ccr-cap}* helps you create and manage the {ccr} process. +If you want to replicate data from existing indices, or set up +local followers on a case-by-case basis, go to *Follower indices*. +If you want to automatically detect and follow new indices when they are created +on a remote cluster, you can do so from *Auto-follow patterns*. -Creating an auto-follow pattern is useful when you have time-series data, like a logs index, on the -remote cluster that is created or rolled over on a daily basis. Once you have configured an -auto-follow pattern, any time a new index with a name that matches the pattern is +Creating an auto-follow pattern is useful when you have time-series data, like a logs index, on the +remote cluster that is created or rolled over on a daily basis. Once you have configured an +auto-follow pattern, any time a new index with a name that matches the pattern is created in the remote cluster, a follower index is automatically configured in the local cluster. -From the same view, you can also see a list of your saved auto-follow patterns for +From the same view, you can also see a list of your saved auto-follow patterns for a given remote cluster, and monitor whether the replication is active. Before you use these features, you should be familiar with the following concepts: -* {ref}/ccr-requirements.html[Requirements for leader indices] +* {ref}/ccr-requirements.html[Requirements for leader indices] * {ref}/ccr-auto-follow.html[Automatically following indices] -To get started, go to *Management > Elasticsearch > {ccr-cap}*. +To get started, go to *Management > Elasticsearch > {ccr-cap}*. [role="screenshot"] image::images/auto_follow_pattern.png[][UI for adding an auto-follow pattern] diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 2daa4cf789f2a..a92a6ae4bdc09 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -1,9 +1,9 @@ [[managing-saved-objects]] -== Saved objects +== Saved Objects -*Saved Objects* helps you keep track of and manage your saved objects. These objects +The *Saved Objects* UI helps you keep track of and manage your saved objects. These objects store data for later use, including dashboards, visualizations, maps, index patterns, -Canvas workpads, and more. +Canvas workpads, and more. To get started, go to *Management > {kib} > Saved Objects*. With this UI, you can: @@ -23,8 +23,8 @@ image::images/management-saved-objects.png[Saved Objects] * To view and edit an object in its associated application, click the object title. -* To show objects that use this object, so you know the -impact of deleting it, click the actions icon image:images/actions_icon.png[Actions icon] +* To show objects that use this object, so you know the +impact of deleting it, click the actions icon image:images/actions_icon.png[Actions icon] and select *Relationships*. * To delete one or more objects, select their checkboxes, and then click *Delete*. @@ -33,19 +33,19 @@ and select *Relationships*. [[managing-saved-objects-export-objects]] === Import and export -Using the import and export commands, you can move objects between different -{kib} instances. This action is useful when you -have multiple environments for development and production. -Import and export also work well when you have a large number -of objects to update and want to batch the process. +Using the import and export commands, you can move objects between different +{kib} instances. This action is useful when you +have multiple environments for development and production. +Import and export also work well when you have a large number +of objects to update and want to batch the process. [float] ==== Import -You can import multiple objects in a single operation. Click *Import* and -navigate to the NDJSON file that -represents the objects to import. By default, +You can import multiple objects in a single operation. Click *Import* and +navigate to the NDJSON file that +represents the objects to import. By default, saved objects already in {kib} are overwritten. [float] @@ -56,7 +56,7 @@ You have two options for exporting saved objects. * Select the checkboxes of objects that you want to export, and then click *Export*. * Click *Export x objects*, and export objects by type. -This action creates an NDJSON with all your saved objects. By default, +This action creates an NDJSON with all your saved objects. By default, the NDJSON includes related objects. Exported dashboards include their associated index patterns. [float] @@ -78,9 +78,9 @@ use the <> inste === Advanced editing Some objects offer an advanced *Edit* page for modifying the object definition. -To open the page, click the actions icon image:images/actions_icon.png[Actions icon] -and select *Inspect*. -You can change the object title, add a description, and modify +To open the page, click the actions icon image:images/actions_icon.png[Actions icon] +and select *Inspect*. +You can change the object title, add a description, and modify the JSON that defines the object properties. If you access an object whose index has been deleted, you can: @@ -90,7 +90,7 @@ If you access an object whose index has been deleted, you can: * Change the index name in the object's `reference` array to point to an existing index pattern. This is useful if the index you were working with has been renamed. -WARNING: Validation is not performed for object properties. Submitting an invalid -change will render the object unusable. A more failsafe approach is to use -*Discover*, *Visualize*, or *Dashboard* to create new objects instead of +WARNING: Validation is not performed for object properties. Submitting an invalid +change will render the object unusable. A more failsafe approach is to use +*Discover*, *Visualize*, or *Dashboard* to create new objects instead of directly editing an existing one. diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 06983c01f926d..b07f075f88032 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -1,14 +1,14 @@ [role="xpack"] [[data-rollups]] -== Rollup jobs +== Rollup Jobs -A rollup job is a periodic task that aggregates data from indices specified -by an index pattern and rolls it into a new index. Rollup indices are a good way to -compactly store months or years of historical +A rollup job is a periodic task that aggregates data from indices specified +by an index pattern, and then rolls it into a new index. Rollup indices are a good way to +compactly store months or years of historical data for use in visualizations and reports. -You’ll find *Rollup Jobs* under *Management > Elasticsearch*. With this UI, +You’ll find *Rollup Jobs* under *Management > Elasticsearch*. With this UI, you can: * <> @@ -17,22 +17,22 @@ you can: [role="screenshot"] image::images/management_rollup_list.png[][List of currently active rollup jobs] -Before using this feature, you should be familiar with how rollups work. -{ref}/xpack-rollup.html[Rolling up historical data] is a good source for more detailed information. +Before using this feature, you should be familiar with how rollups work. +{ref}/xpack-rollup.html[Rolling up historical data] is a good source for more detailed information. [float] [[create-and-manage-rollup-job]] === Create a rollup job -{kib} makes it easy for you to create a rollup job by walking you through -the process. You fill in the name, data flow, and how often you want to roll -up the data. Then you define a date histogram aggregation for the rollup job -and optionally terms, histogram, and metrics aggregations. +{kib} makes it easy for you to create a rollup job by walking you through +the process. You fill in the name, data flow, and how often you want to roll +up the data. Then you define a date histogram aggregation for the rollup job +and optionally define terms, histogram, and metrics aggregations. -When defining the index pattern, you must enter a name that is different than -the output rollup index. Otherwise, the job -will attempt to capture the data in the rollup index. For example, if your index pattern is `metricbeat-*`, -you can name your rollup index `rollup-metricbeat`, but not `metricbeat-rollup`. +When defining the index pattern, you must enter a name that is different than +the output rollup index. Otherwise, the job +will attempt to capture the data in the rollup index. For example, if your index pattern is `metricbeat-*`, +you can name your rollup index `rollup-metricbeat`, but not `metricbeat-rollup`. [role="screenshot"] image::images/management_create_rollup_job.png[][Wizard that walks you through creation of a rollup job] @@ -41,38 +41,38 @@ image::images/management_create_rollup_job.png[][Wizard that walks you through c [[manage-rollup-job]] === Start, stop, and delete rollup jobs -Once you’ve saved a rollup job, you’ll see it the *Rollup Jobs* overview page, -where you can drill down for further investigation. The *Manage* menu in +Once you’ve saved a rollup job, you’ll see it the *Rollup Jobs* overview page, +where you can drill down for further investigation. The *Manage* menu in the lower right enables you to start, stop, and delete the rollup job. You must first stop a rollup job before deleting it. [role="screenshot"] image::images/management_rollup_job_details.png[][Rollup job details] -You can’t change a rollup job after you’ve created it. To select additional fields -or redefine terms, you must delete the existing job, and then create a new one -with the updated specifications. Be sure to use a different name for the new rollup -job—reusing the same name can lead to problems with mismatched job configurations. -You can read more at {ref}/rollup-job-config.html[rollup job configuration]. +You can’t change a rollup job after you’ve created it. To select additional fields +or redefine terms, you must delete the existing job, and then create a new one +with the updated specifications. Be sure to use a different name for the new rollup +job—reusing the same name can lead to problems with mismatched job configurations. +You can read more at {ref}/rollup-job-config.html[rollup job configuration]. [float] === Try it: Create and visualize rolled up data -This example creates a rollup job to capture log data from sample web logs. +This example creates a rollup job to capture log data from sample web logs. To follow along, add the <>. In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` -to roll up once a day into the index `rollup_logstash`. You’ll bucket the -rolled up data on an hourly basis, using 60m for the time bucket configuration. +to roll up once a day into the index `rollup_logstash`. You’ll bucket the +rolled up data on an hourly basis, using 60m for the time bucket configuration. This allows for more granular queries, such as 2h and 12h. [float] ==== Create the rollup job -As you walk through the *Create rollup job* UI, enter the data shown in -the table below. The terms, histogram, and metrics fields reflect -the key information to retain in the rolled up data: where visitors are from (geo.src), -what operating system they are using (machine.os.keyword), +As you walk through the *Create rollup job* UI, enter the data shown in +the table below. The terms, histogram, and metrics fields reflect +the key information to retain in the rolled up data: where visitors are from (geo.src), +what operating system they are using (machine.os.keyword), and how much data is being sent (bytes). |=== @@ -118,31 +118,28 @@ and how much data is being sent (bytes). |=== -You can now use the rolled up data for analysis at a fraction of the storage cost -of the original index. The original data can live side by side with the new +You can now use the rolled up data for analysis at a fraction of the storage cost +of the original index. The original data can live side by side with the new rollup index, or you can remove or archive it using <>. [float] ==== Visualize the rolled up data -Your next step is to visualize your rolled up data in a vertical bar chart. +Your next step is to visualize your rolled up data in a vertical bar chart. Most visualizations support rolled up data, with the exception of Timelion, TSVB, and Vega visualizations. -Using the information from the example rollup configuration described above, -you can use `rollup_logstash` to match the rolled up index pattern, -and `kibana_sample_data_logs` to match the index pattern for raw data. -The notation for a combination index pattern with both raw and rolled up data +Using the information from the example rollup configuration described above, +you can use `rollup_logstash` to match the rolled up index pattern, +and `kibana_sample_data_logs` to match the index pattern for raw data. +The notation for a combination index pattern with both raw and rolled up data is `rollup_logstash,kibana_sample_data_logs`. [role="screenshot"] image::images/management_rollup_job_vis.png[][Visualization of rolled up data] -You can then create a dashboard that contains visualizations of the rolled up +You can then create a dashboard that contains visualizations of the rolled up data, raw data, or both. See <> for more information. [role="screenshot"] image::images/management_rollup_job_dashboard.png[][Dashboard with rolled up data] - - - diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index ebe6c10c49872..3911d57e05c9a 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -38,14 +38,6 @@ include::monitoring/index.asciidoc[] include::management.asciidoc[] -include::{kib-repo-dir}/spaces/index.asciidoc[] - -include::security/index.asciidoc[] - -include::{kib-repo-dir}/management/watcher-ui/index.asciidoc[] - -include::{kib-repo-dir}/management/upgrade-assistant/index.asciidoc[] - include::reporting/index.asciidoc[] include::api.asciidoc[] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 2c41d0072fe5b..1c55ffc73ca72 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -3,17 +3,115 @@ [partintro] -- -The Management application is where you perform your runtime configuration of -Kibana, including both the initial setup and ongoing configuration of index -patterns, advanced settings that tweak the behaviors of Kibana itself, and -the various "objects" that you can save throughout Kibana such as searches, -visualizations, and dashboards. +*Management* is home to UIs for managing all things Elastic Stack— +indices, clusters, licenses, UI settings, index patterns, spaces, and more. + +[float] +[[manage-Elasticsearch]] +== Manage {es} + +[cols="50, 50"] +|=== + +a| <> + +Replicate indices on a remote cluster and copy them to a follower index on a local cluster. +This is important for +disaster recovery. It also keeps data local for faster queries. + +| <> + +Create a policy for defining the lifecycle of an index as it ages +through the hot, warm, cold, and delete phases. +Such policies help you control operation costs +because you can put data in different resource tiers. + +a| <> + +View index settings, mappings, and statistics and perform operations, such as refreshing, +flushing, and clearing the cache. Practicing good index management ensures +that your data is stored cost effectively. + +| <> + +View the status of your license, start a trial, or install a new license. For +the full list of features that are included in your license, +see the https://www.elastic.co/subscriptions[subscription page]. + +| <> + +Manage your remote clusters for use with cross-cluster search and cross-cluster replication. +You can add and remove remote clusters, and check their connectivity. + +| <> + +Create a job that periodically aggregates data from one or more indices, and then +rolls it into a new, compact index. Rollup indices are a good way to store months or +years of historical data in combination with your raw data. + +| <> + +Define a policy that creates, schedules, and automatically deletes snapshots to ensure that you +have backups of your cluster in case something goes wrong. + +| {ref}/transforms.html[*Transforms*] + +Use transforms to pivot existing {es} indices into summarized or entity-centric indices. + +| <> + +Identify the issues that you need to address before upgrading to the +next major version of {es}, and then reindex, if needed. + +| <> + +Detect changes in your data by creating, managing, and monitoring alerts. +For example, create an alert when the maximum total CPU usage on a machine goes +above a certain percentage. + +|=== + +[float] +[[manage-kibana]] +== Manage {kib} + +[cols="50, 50"] +|=== + +a| <> + +Customize {kib} to suit your needs. Change the format for displaying dates, turn on dark mode, +set the timespan for notification messages, and much more. + +| <> + +Create and manage the index patterns that help you retrieve your data from {es}. + +| <> + +Monitor the generation of reports—PDF, PNG, and CSV—and download reports that you previously generated. +A report can contain a dashboard, visualization, saved search, or Canvas workpad. + +| <> + +Copy, edit, delete, import, and export your saved objects. +These include dashboards, visualizations, maps, index patterns, Canvas workpads, and more. + +| <> + +Create spaces to organize your dashboards and other saved objects into categories. +A space is isolated from all other spaces, +so you can tailor it to your needs without impacting others. + +|   + +|=== -- -include::{kib-repo-dir}/management/managing-licenses.asciidoc[] +include::{kib-repo-dir}/management/advanced-options.asciidoc[] -include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[] +include::{kib-repo-dir}/management/managing-beats.asciidoc[] include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] @@ -25,16 +123,24 @@ include::{kib-repo-dir}/management/index-lifecycle-policies/add-policy-to-index. include::{kib-repo-dir}/management/index-lifecycle-policies/example-index-lifecycle-policy.asciidoc[] +include::{kib-repo-dir}/management/managing-indices.asciidoc[] + include::{kib-repo-dir}/management/managing-fields.asciidoc[] -include::{kib-repo-dir}/management/managing-indices.asciidoc[] +include::{kib-repo-dir}/management/managing-licenses.asciidoc[] -include::{kib-repo-dir}/management/advanced-options.asciidoc[] +include::{kib-repo-dir}/management/managing-remote-clusters.asciidoc[] -include::{kib-repo-dir}/management/managing-saved-objects.asciidoc[] +include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[] -include::{kib-repo-dir}/management/managing-beats.asciidoc[] +include::{kib-repo-dir}/management/managing-saved-objects.asciidoc[] -include::{kib-repo-dir}/management/managing-remote-clusters.asciidoc[] +include::security/index.asciidoc[] include::{kib-repo-dir}/management/snapshot-restore/index.asciidoc[] + +include::{kib-repo-dir}/spaces/index.asciidoc[] + +include::{kib-repo-dir}/management/upgrade-assistant/index.asciidoc[] + +include::{kib-repo-dir}/management/watcher-ui/index.asciidoc[] diff --git a/package.json b/package.json index 305aeba900503..539d3a067665f 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "@babel/core": "^7.5.5", "@babel/register": "^7.7.0", "@elastic/apm-rum": "^4.6.0", - "@elastic/charts": "^16.1.0", + "@elastic/charts": "^17.0.2", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", "@elastic/eui": "18.3.0", @@ -146,11 +146,11 @@ "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "angular": "^1.7.9", - "angular-aria": "^1.7.8", + "angular-aria": "^1.7.9", "angular-elastic": "^2.5.1", "angular-recursion": "^1.0.5", - "angular-route": "^1.7.8", - "angular-sanitize": "^1.7.8", + "angular-route": "^1.7.9", + "angular-sanitize": "^1.7.9", "angular-sortable-view": "^0.0.17", "autoprefixer": "9.6.1", "babel-loader": "^8.0.6", @@ -380,7 +380,7 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^2.15.0", "@typescript-eslint/parser": "^2.15.0", - "angular-mocks": "^1.7.8", + "angular-mocks": "^1.7.9", "archiver": "^3.1.1", "axe-core": "^3.3.2", "babel-eslint": "^10.0.3", @@ -485,7 +485,7 @@ "zlib": "^1.0.5" }, "engines": { - "node": "10.18.0", + "node": "10.19.0", "yarn": "^1.21.1" } } diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 8c8b8b8a21488..fc9d159ea9b95 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,9 +9,9 @@ "kbn:watch": "node scripts/build --watch" }, "devDependencies": { + "@elastic/charts": "^17.0.2", "abort-controller": "^3.0.0", "@elastic/eui": "18.3.0", - "@elastic/charts": "^16.1.0", "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", "@yarnpkg/lockfile": "^1.1.0", diff --git a/src/core/TESTING.md b/src/core/TESTING.md index 23c0879e4411e..4dfab8830a506 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -548,9 +548,300 @@ _How to test SO operations_ _How to test ES clients_ -## Plugin Integrations +## Plugin integrations -_How to test against specific plugin APIs (eg. data plugin)_ +In the new platform, all plugin's dependencies to other plugins are explicitly declared in their `kibana.json` +manifest. As for `core`, the dependencies `setup` and `start` contracts are injected in your plugin's respective +`setup` and `start` phases. One of the upsides with testing is that every usage of the dependencies is explicit, +and that the plugin's contracts must be propagated to the parts of the code using them, meaning that isolating a +specific logical component for unit testing is way easier than in legacy. + +The approach to test parts of a plugin's code that is relying on other plugins is quite similar to testing +code using `core` APIs: it's expected to mock the dependency, and make it return the value the test is expecting. + +Most plugins are defining mocks for their contracts. The convention is to expose them in a `mocks` file in +`my_plugin/server` and/or `my_plugin/public`. For example for the `data` plugin, the client-side mocks are located in +`src/plugins/data/public/mocks.ts`. When such mocks are present, it's strongly recommended to use them +when testing against dependencies. Otherwise, one should create it's own mocked implementation of the dependency's +contract (and should probably ping the plugin's owner to ask them to add proper contract mocks). + +### Preconditions + +For these examples, we are going to see how we should test the `myPlugin` plugin. + +This plugin declares the `data` plugin as a `required` dependency and the `usageCollection` plugin as an `optional` +one. It also exposes a `getSpecialSuggestions` API in it's start contract, which relies on the `data` plugin to retrieve +data. + +`MyPlugin` plugin definition: + +```typescript +// src/plugins/myplugin/public/plugin.ts +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; +import { SuggestionsService } from './suggestions'; + +interface MyPluginSetupDeps { + data: DataPublicPluginSetup; + usageCollection?: UsageCollectionSetup; +} + +interface MyPluginStartDeps { + data: DataPublicPluginStart; +} + +export class MyPlugin implements Plugin { + private suggestionsService = new SuggestionsService(); + + public setup(core: CoreSetup, { data, usageCollection }: MyPluginSetupDeps) { + // setup our internal service + this.suggestionsService.setup(data); + + // an example on using an optional dependency that will be tested + if (usageCollection) { + usageCollection.allowTrackUserAgent(true); + } + + return {}; + } + + public start(core: CoreStart, { data }: MyPluginStartDeps) { + const suggestions = this.suggestionsService.start(data); + return { + getSpecialSuggestions: (query: string) => suggestions.getSuggestions(query), + }; + } + + public stop() {} +} + +export type MyPluginSetup = ReturnType; +export type MyPluginStart = ReturnType; +``` + +The underlying `SuggestionsService` implementation: + +```typescript +// src/plugins/myplugin/public/suggestions/suggestion_service.ts +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../data/public'; + +// stubs for testing purposes +const suggestDependingOn = (...args: any[]) => []; +const baseOptions = {} as any; +export const defaultSuggestions = [ + { + text: 'a default suggestion', + }, +] as any[]; + +export class SuggestionsService { + public setup(data: DataPublicPluginSetup) { + // register a suggestion provider to the `data` dependency plugin + data.autocomplete.addQuerySuggestionProvider('fr', async args => { + return suggestDependingOn(args); + }); + } + + public start(data: DataPublicPluginStart) { + return { + getSuggestions: async (query: string) => { + // use the `data` plugin contract to retrieve arbitrary data + // note: this logic does not really make any sense and is only here to introduce a behavior to test + const baseSuggestions = await data.autocomplete.getQuerySuggestions({ + ...baseOptions, + query, + }); + if (!baseSuggestions || baseSuggestions.length === 0) { + return defaultSuggestions; + } + return baseSuggestions.filter(suggestion => suggestion.type !== 'conjunction'); + }, + }; + } +} +``` + +### Testing dependencies usages + +A plugin should test expected usage and calls on it's dependency plugins' API. + +Some calls, such as 'registration' APIs exposed from dependency plugins, should be checked, +to ensure both that they are actually executed, and performed with the correct parameters. + +For our example plugin's `SuggestionsService`, we should assert that the suggestion provider is correctly +registered to the `data` plugin during the `setup` phase, and that `getSuggestions` calls +`autocomplete.getQuerySuggestions` with the correct parameters. + +```typescript +// src/plugins/myplugin/public/suggestions/suggestion_service.test.ts +import { + dataPluginMock, + Setup as DataPluginSetupMock, + Start as DataPluginStartMock, +} from '../../../data/public/mocks'; +import { SuggestionsService } from './suggestion_service'; + +describe('SuggestionsService', () => { + let service: SuggestionsService; + let dataSetup: DataPluginSetupMock; + let dataStart: DataPluginStartMock; + + beforeEach(() => { + service = new SuggestionsService(); + dataSetup = dataPluginMock.createSetupContract(); + dataStart = dataPluginMock.createStartContract(); + }); + + describe('#setup', () => { + it('registers the query suggestion provider to the data plugin', () => { + service.setup(dataSetup); + + expect(dataSetup.autocomplete.addQuerySuggestionProvider).toHaveBeenCalledTimes(1); + expect(dataSetup.autocomplete.addQuerySuggestionProvider).toHaveBeenCalledWith( + 'fr', + expect.any(Function) + ); + }); + }); + + describe('#start', () => { + describe('#getSuggestions', () => { + it('calls getQuerySuggestions with the correct query', async () => { + service.setup(dataSetup); + const serviceStart = service.start(dataStart); + + await serviceStart.getSuggestions('some query'); + + expect(dataStart.autocomplete.getQuerySuggestions).toHaveBeenCalledTimes(1); + expect(dataStart.autocomplete.getQuerySuggestions).toHaveBeenCalledWith( + expect.objectContaining({ + query: 'some query', + }) + ); + }); + }); + }); +}); +``` + +### Testing components consuming the dependencies + +When testing parts of your plugin code that depends on the dependency plugin's data, the best approach +is to mock the dependency to be able to get the behavior expected for the test. + +In this example, we are going to mock the results of `autocomplete.getQuerySuggestions` to be able to test +the service's `getSuggestions` method. + +```typescript +// src/plugins/myplugin/public/suggestions/suggestion_service.ts + +describe('#start', () => { + describe('#getSuggestions', () => { + it('returns the default suggestions when autocomplete returns no results', async () => { + dataStart.autocomplete.getQuerySuggestions.mockResolvedValue([]); + + service.setup(dataSetup); + const serviceStart = service.start(dataStart); + + const results = await serviceStart.getSuggestions('some query'); + expect(results).toEqual(defaultSuggestions); + }); + + it('excludes conjunctions from the autocomplete results', async () => { + dataStart.autocomplete.getQuerySuggestions.mockResolvedValue([ + { + type: 'field', + text: 'field suggestion', + }, + { + type: 'conjunction', + text: 'conjunction suggestion', + }, + ]); + + service.setup(dataSetup); + const serviceStart = service.start(dataStart); + + const results = await serviceStart.getSuggestions('some query'); + + expect(results).toEqual([ + { + type: 'field', + text: 'field suggestion', + }, + ]); + }); + }); +}); +``` + +### Testing optional plugin dependencies + +Plugins should test that their behavior remains correct when their optional dependencies are either available or not. + +A basic test would be to ensure that the plugin properly initialize without error when the optional +dependency is missing: + +```typescript +// src/plugins/myplugin/public/plugin.test.ts +import { coreMock } from '../../../core/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; +import { MyPlugin } from './plugin'; + +describe('Plugin', () => { + it('initializes correctly if usageCollection is disabled', () => { + const plugin = new MyPlugin(coreMock.createPluginInitializerContext()); + const coreSetup = coreMock.createSetup(); + const setupDeps = { + data: dataPluginMock.createSetupContract(), + // optional usageCollector dependency is not available + }; + + const coreStart = coreMock.createStart(); + const startDeps = { + data: dataPluginMock.createStartContract(), + }; + + expect(() => { + plugin.setup(coreSetup, setupDeps); + }).not.toThrow(); + expect(() => { + plugin.start(coreStart, startDeps); + }).not.toThrow(); + }); +}); +``` + +Then we should test that when optional dependency is properly used when present: + +```typescript +// src/plugins/myplugin/public/plugin.test.ts +import { coreMock } from '../../../core/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; +import { usageCollectionPluginMock } from '../../usage_collection/public/mocks'; + +import { MyPlugin } from './plugin'; + +describe('Plugin', () => { + // [...] + + it('enables trackUserAgent when usageCollection is available', async () => { + const plugin = new MyPlugin(coreMock.createPluginInitializerContext()); + const coreSetup = coreMock.createSetup(); + const usageCollectionSetup = usageCollectionPluginMock.createSetupContract(); + const setupDeps = { + data: dataPluginMock.createSetupContract(), + usageCollection: usageCollectionSetup, + }; + + plugin.setup(coreSetup, setupDeps); + + expect(usageCollectionSetup.allowTrackUserAgent).toHaveBeenCalledTimes(1); + expect(usageCollectionSetup.allowTrackUserAgent).toHaveBeenCalledWith(true); + }); +}); +``` ## Plugin Contracts diff --git a/src/core/server/index.ts b/src/core/server/index.ts index c45acd7f0129a..cc838ddd1351d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -197,6 +197,7 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectsMigrationLogger, SavedObjectsRawDoc, + SavedObjectSanitizedDoc, SavedObjectsRepositoryFactory, SavedObjectsResolveImportErrorsOptions, SavedObjectsSchema, @@ -211,6 +212,15 @@ export { SavedObjectsRepository, SavedObjectsDeleteByNamespaceOptions, SavedObjectsIncrementCounterOptions, + SavedObjectsComplexFieldMapping, + SavedObjectsCoreFieldMapping, + SavedObjectsFieldMapping, + SavedObjectsTypeMappingDefinition, + SavedObjectsMappingProperties, + SavedObjectTypeRegistry, + SavedObjectsType, + SavedObjectMigrationMap, + SavedObjectMigrationFn, } from './saved_objects'; export { diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index 495141cdcb58d..44405dc391d8e 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -18,13 +18,18 @@ */ import { LegacyService } from './legacy_service'; -import { LegacyServiceDiscoverPlugins, LegacyServiceSetupDeps } from './types'; +import { LegacyConfig, LegacyServiceDiscoverPlugins, LegacyServiceSetupDeps } from './types'; type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>; const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({ pluginSpecs: [], - uiExports: {} as any, + uiExports: { + savedObjectSchemas: {}, + savedObjectMappings: [], + savedObjectMigrations: {}, + savedObjectValidations: {}, + }, navLinks: [], pluginExtendedConfig: { get: jest.fn(), @@ -34,6 +39,7 @@ const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({ disabledPluginSpecs: [], settings: {}, }); + const createLegacyServiceMock = (): LegacyServiceMock => ({ legacyId: Symbol(), discoverPlugins: jest.fn().mockResolvedValue(createDiscoverPluginsMock()), @@ -42,8 +48,15 @@ const createLegacyServiceMock = (): LegacyServiceMock => ({ stop: jest.fn(), }); +const createLegacyConfigMock = (): jest.Mocked => ({ + get: jest.fn(), + has: jest.fn(), + set: jest.fn(), +}); + export const legacyServiceMock = { create: createLegacyServiceMock, createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps), createDiscoverPlugins: createDiscoverPluginsMock, + createLegacyConfig: createLegacyConfigMock, }; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index af4db68ee95e1..e8e20580a36db 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -83,7 +83,7 @@ beforeEach(() => { getAuthHeaders: () => undefined, } as any, }, - savedObjects: savedObjectsServiceMock.createSetupContract(), + savedObjects: savedObjectsServiceMock.createInternalSetupContract(), plugins: { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { @@ -101,7 +101,7 @@ beforeEach(() => { startDeps = { core: { capabilities: capabilitiesServiceMock.createStartContract(), - savedObjects: savedObjectsServiceMock.createStartContract(), + savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), plugins: { contracts: new Map() }, }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index f9b18afadc938..b2501496d87ef 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -262,6 +262,7 @@ export class LegacyService implements CoreService { getScopedClient: startDeps.core.savedObjects.getScopedClient, createScopedRepository: startDeps.core.savedObjects.createScopedRepository, createInternalRepository: startDeps.core.savedObjects.createInternalRepository, + createSerializer: startDeps.core.savedObjects.createSerializer, }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, }; @@ -328,6 +329,7 @@ export class LegacyService implements CoreService { __internals: { hapiServer: setupDeps.core.http.server, kibanaMigrator: startDeps.core.savedObjects.migrator, + typeRegistry: startDeps.core.savedObjects.typeRegistry, uiPlugins: setupDeps.core.plugins.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, rendering: setupDeps.core.rendering, diff --git a/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap b/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap index 6ffc1ea140beb..3c40362e8211e 100644 --- a/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap +++ b/src/core/server/legacy/logging/appenders/__snapshots__/legacy_appender.test.ts.snap @@ -1,127 +1,142 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`\`append()\` correctly pushes records to legacy platform. 1`] = ` -Array [ - Array [ - Object { - "context": "context-1", - "level": LogLevel { - "id": "trace", - "value": 7, - }, - "message": "message-1", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-2", - "level": LogLevel { - "id": "debug", - "value": 6, - }, - "message": "message-2", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-3.sub-context-3", - "level": LogLevel { - "id": "info", - "value": 5, - }, - "message": "message-3", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-4.sub-context-4", - "level": LogLevel { - "id": "warn", - "value": 4, - }, - "message": "message-4", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-5", - "error": [Error: Some Error], - "level": LogLevel { - "id": "error", - "value": 3, - }, - "message": "message-5-with-error", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-6", - "level": LogLevel { - "id": "error", - "value": 3, - }, - "message": "message-6-with-message", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-7.sub-context-7.sub-sub-context-7", - "error": [Error: Some Fatal Error], - "level": LogLevel { - "id": "fatal", - "value": 2, - }, - "message": "message-7-with-error", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-8.sub-context-8.sub-sub-context-8", - "level": LogLevel { - "id": "fatal", - "value": 2, - }, - "message": "message-8-with-message", - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-9.sub-context-9", - "level": LogLevel { - "id": "info", - "value": 5, - }, - "message": "message-9-with-message", - "meta": Object { - "someValue": 3, - }, - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], - Array [ - Object { - "context": "context-10.sub-context-10", - "level": LogLevel { - "id": "info", - "value": 5, - }, - "message": "message-10-with-message", - "meta": Object { - "tags": Array [ - "tag1", - "tag2", - ], - }, - "timestamp": 2012-02-01T11:22:33.044Z, - }, - ], -] +Object { + "context": "context-1", + "level": LogLevel { + "id": "trace", + "value": 7, + }, + "message": "message-1", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 2`] = ` +Object { + "context": "context-2", + "level": LogLevel { + "id": "debug", + "value": 6, + }, + "message": "message-2", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 3`] = ` +Object { + "context": "context-3.sub-context-3", + "level": LogLevel { + "id": "info", + "value": 5, + }, + "message": "message-3", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 4`] = ` +Object { + "context": "context-4.sub-context-4", + "level": LogLevel { + "id": "warn", + "value": 4, + }, + "message": "message-4", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 5`] = ` +Object { + "context": "context-5", + "error": [Error: Some Error], + "level": LogLevel { + "id": "error", + "value": 3, + }, + "message": "message-5-with-error", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 6`] = ` +Object { + "context": "context-6", + "level": LogLevel { + "id": "error", + "value": 3, + }, + "message": "message-6-with-message", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 7`] = ` +Object { + "context": "context-7.sub-context-7.sub-sub-context-7", + "error": [Error: Some Fatal Error], + "level": LogLevel { + "id": "fatal", + "value": 2, + }, + "message": "message-7-with-error", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 8`] = ` +Object { + "context": "context-8.sub-context-8.sub-sub-context-8", + "level": LogLevel { + "id": "fatal", + "value": 2, + }, + "message": "message-8-with-message", + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 9`] = ` +Object { + "context": "context-9.sub-context-9", + "level": LogLevel { + "id": "info", + "value": 5, + }, + "message": "message-9-with-message", + "meta": Object { + "someValue": 3, + }, + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} +`; + +exports[`\`append()\` correctly pushes records to legacy platform. 10`] = ` +Object { + "context": "context-10.sub-context-10", + "level": LogLevel { + "id": "info", + "value": 5, + }, + "message": "message-10-with-message", + "meta": Object { + "tags": Array [ + "tag1", + "tag2", + ], + }, + "pid": Any, + "timestamp": 2012-02-01T11:22:33.044Z, +} `; diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts index adc5dcae3ec9d..538d987e781d0 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts @@ -46,24 +46,28 @@ test('`append()` correctly pushes records to legacy platform.', () => { level: LogLevel.Trace, message: 'message-1', timestamp, + pid: 5355, }, { context: 'context-2', level: LogLevel.Debug, message: 'message-2', timestamp, + pid: 5355, }, { context: 'context-3.sub-context-3', level: LogLevel.Info, message: 'message-3', timestamp, + pid: 5355, }, { context: 'context-4.sub-context-4', level: LogLevel.Warn, message: 'message-4', timestamp, + pid: 5355, }, { context: 'context-5', @@ -71,12 +75,14 @@ test('`append()` correctly pushes records to legacy platform.', () => { level: LogLevel.Error, message: 'message-5-with-error', timestamp, + pid: 5355, }, { context: 'context-6', level: LogLevel.Error, message: 'message-6-with-message', timestamp, + pid: 5355, }, { context: 'context-7.sub-context-7.sub-sub-context-7', @@ -84,18 +90,21 @@ test('`append()` correctly pushes records to legacy platform.', () => { level: LogLevel.Fatal, message: 'message-7-with-error', timestamp, + pid: 5355, }, { context: 'context-8.sub-context-8.sub-sub-context-8', level: LogLevel.Fatal, message: 'message-8-with-message', timestamp, + pid: 5355, }, { context: 'context-9.sub-context-9', level: LogLevel.Info, message: 'message-9-with-message', timestamp, + pid: 5355, meta: { someValue: 3 }, }, { @@ -103,6 +112,7 @@ test('`append()` correctly pushes records to legacy platform.', () => { level: LogLevel.Info, message: 'message-10-with-message', timestamp, + pid: 5355, meta: { tags: ['tag1', 'tag2'] }, }, ]; @@ -113,7 +123,12 @@ test('`append()` correctly pushes records to legacy platform.', () => { } const [mockLegacyLoggingServerInstance] = (LegacyLoggingServer as any).mock.instances; - expect(mockLegacyLoggingServerInstance.log.mock.calls).toMatchSnapshot(); + expect(mockLegacyLoggingServerInstance.log.mock.calls).toHaveLength(records.length); + records.forEach((r, idx) => { + expect(mockLegacyLoggingServerInstance.log.mock.calls[idx][0]).toMatchSnapshot({ + pid: expect.any(Number), + }); + }); }); test('legacy logging server is correctly created and disposed.', async () => { diff --git a/src/core/server/legacy/logging/legacy_logging_server.test.ts b/src/core/server/legacy/logging/legacy_logging_server.test.ts index beb2098600ce6..6dca3a199728e 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.test.ts +++ b/src/core/server/legacy/logging/legacy_logging_server.test.ts @@ -31,6 +31,7 @@ test('correctly forwards log records.', () => { const timestamp = 1554433221100; const firstLogRecord = { timestamp: new Date(timestamp), + pid: 5355, level: LogLevel.Info, context: 'some-context', message: 'some-message', @@ -38,6 +39,7 @@ test('correctly forwards log records.', () => { const secondLogRecord = { timestamp: new Date(timestamp), + pid: 5355, level: LogLevel.Error, context: 'some-context.sub-context', message: 'some-message', @@ -47,6 +49,7 @@ test('correctly forwards log records.', () => { const thirdLogRecord = { timestamp: new Date(timestamp), + pid: 5355, level: LogLevel.Trace, context: 'some-context.sub-context', message: 'some-message', diff --git a/src/core/server/logging/__snapshots__/logging_service.test.ts.snap b/src/core/server/logging/__snapshots__/logging_service.test.ts.snap index ffde6cbcdebd1..54c170f523299 100644 --- a/src/core/server/logging/__snapshots__/logging_service.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_service.test.ts.snap @@ -1,57 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`appends records via multiple appenders.: console logs 1`] = ` -Array [ - Array [ - "[2012-02-01T00:00:00.000Z][INFO ][some-context] You know, just for your info.", - ], -] -`; +exports[`appends records via multiple appenders.: console logs 1`] = `"[2012-02-01T00:00:00.000Z][INFO ][some-context] You know, just for your info."`; exports[`appends records via multiple appenders.: file logs 1`] = ` -Array [ - Array [ - "[2012-02-01T00:00:00.000Z][WARN ][tests] Config is not ready! -", - ], - Array [ - "[2012-02-01T00:00:00.000Z][ERROR][tests.child] Too bad that config is not ready :/ -", - ], -] +"[2012-02-01T00:00:00.000Z][WARN ][tests] Config is not ready! +" +`; + +exports[`appends records via multiple appenders.: file logs 2`] = ` +"[2012-02-01T00:00:00.000Z][ERROR][tests.child] Too bad that config is not ready :/ +" `; exports[`asLoggerFactory() only allows to create new loggers. 1`] = ` -Array [ - Array [ - "{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"test.context\\",\\"level\\":\\"TRACE\\",\\"message\\":\\"buffered trace message\\"}", - ], - Array [ - "{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"test.context\\",\\"level\\":\\"INFO\\",\\"message\\":\\"buffered info message\\",\\"meta\\":{\\"some\\":\\"value\\"}}", - ], - Array [ - "{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"test.context\\",\\"level\\":\\"FATAL\\",\\"message\\":\\"buffered fatal message\\"}", - ], -] +Object { + "@timestamp": "2012-02-01T00:00:00.000Z", + "context": "test.context", + "level": "TRACE", + "message": "buffered trace message", + "pid": Any, +} +`; + +exports[`asLoggerFactory() only allows to create new loggers. 2`] = ` +Object { + "@timestamp": "2012-02-01T00:00:00.000Z", + "context": "test.context", + "level": "INFO", + "message": "buffered info message", + "meta": Object { + "some": "value", + }, + "pid": Any, +} +`; + +exports[`asLoggerFactory() only allows to create new loggers. 3`] = ` +Object { + "@timestamp": "2012-02-01T00:00:00.000Z", + "context": "test.context", + "level": "FATAL", + "message": "buffered fatal message", + "pid": Any, +} `; exports[`flushes memory buffer logger and switches to real logger once config is provided: buffered messages 1`] = ` -Array [ - Array [ - "{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"test.context\\",\\"level\\":\\"INFO\\",\\"message\\":\\"buffered info message\\",\\"meta\\":{\\"some\\":\\"value\\"}}", - ], - Array [ - "{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"test.context\\",\\"level\\":\\"FATAL\\",\\"message\\":\\"buffered fatal message\\"}", - ], -] +Object { + "@timestamp": "2012-02-01T00:00:00.000Z", + "context": "test.context", + "level": "INFO", + "message": "buffered info message", + "meta": Object { + "some": "value", + }, + "pid": Any, +} `; exports[`flushes memory buffer logger and switches to real logger once config is provided: new messages 1`] = ` -Array [ - Array [ - "{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"test.context\\",\\"level\\":\\"INFO\\",\\"message\\":\\"some new info message\\"}", - ], -] +Object { + "@timestamp": "2012-02-01T00:00:00.000Z", + "context": "test.context", + "level": "INFO", + "message": "some new info message", + "pid": Any, +} `; exports[`uses \`root\` logger if context is not specified. 1`] = ` @@ -63,32 +77,31 @@ Array [ `; exports[`uses default memory buffer logger until config is provided 1`] = ` -Array [ - Array [ - Object { - "context": "test.context", - "level": LogLevel { - "id": "trace", - "value": 7, - }, - "message": "trace message", - "meta": undefined, - "timestamp": 2012-02-01T00:00:00.000Z, - }, - ], - Array [ - Object { - "context": "test.context2", - "level": LogLevel { - "id": "fatal", - "value": 2, - }, - "message": "fatal message", - "meta": Object { - "some": "value", - }, - "timestamp": 2012-02-01T00:00:00.000Z, - }, - ], -] +Object { + "context": "test.context", + "level": LogLevel { + "id": "trace", + "value": 7, + }, + "message": "trace message", + "meta": undefined, + "pid": Any, + "timestamp": 2012-02-01T00:00:00.000Z, +} +`; + +exports[`uses default memory buffer logger until config is provided 2`] = ` +Object { + "context": "test.context2", + "level": LogLevel { + "id": "fatal", + "value": 2, + }, + "message": "fatal message", + "meta": Object { + "some": "value", + }, + "pid": Any, + "timestamp": 2012-02-01T00:00:00.000Z, +} `; diff --git a/src/core/server/logging/appenders/buffer/buffer_appender.test.ts b/src/core/server/logging/appenders/buffer/buffer_appender.test.ts index 453a29271c582..49d70db8d5d43 100644 --- a/src/core/server/logging/appenders/buffer/buffer_appender.test.ts +++ b/src/core/server/logging/appenders/buffer/buffer_appender.test.ts @@ -34,12 +34,14 @@ test('`flush()` returns all appended records and cleans internal buffer.', () => level: LogLevel.All, message: 'message-1', timestamp: new Date(), + pid: 5355, }, { context: 'context-2', level: LogLevel.Trace, message: 'message-2', timestamp: new Date(), + pid: 5355, }, ]; @@ -64,6 +66,7 @@ test('`dispose()` flushes internal buffer.', async () => { level: LogLevel.All, message: 'message-1', timestamp: new Date(), + pid: 5355, }); await appender.dispose(); diff --git a/src/core/server/logging/appenders/console/console_appender.test.ts b/src/core/server/logging/appenders/console/console_appender.test.ts index 364f0be7fadc3..6e30df1cfb65c 100644 --- a/src/core/server/logging/appenders/console/console_appender.test.ts +++ b/src/core/server/logging/appenders/console/console_appender.test.ts @@ -59,12 +59,14 @@ test('`append()` correctly formats records and pushes them to console.', () => { level: LogLevel.All, message: 'message-1', timestamp: new Date(), + pid: 5355, }, { context: 'context-2', level: LogLevel.Trace, message: 'message-2', timestamp: new Date(), + pid: 5355, }, { context: 'context-3', @@ -72,6 +74,7 @@ test('`append()` correctly formats records and pushes them to console.', () => { level: LogLevel.Fatal, message: 'message-3', timestamp: new Date(), + pid: 5355, }, ]; diff --git a/src/core/server/logging/appenders/file/file_appender.test.ts b/src/core/server/logging/appenders/file/file_appender.test.ts index fccca629f7d60..0483a06b199b6 100644 --- a/src/core/server/logging/appenders/file/file_appender.test.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.ts @@ -70,6 +70,7 @@ test('file stream is created only once and only after first `append()` is called level: LogLevel.All, message: 'message-1', timestamp: new Date(), + pid: 5355, }); expect(mockCreateWriteStream).toHaveBeenCalledTimes(1); @@ -84,6 +85,7 @@ test('file stream is created only once and only after first `append()` is called level: LogLevel.All, message: 'message-2', timestamp: new Date(), + pid: 5355, }); expect(mockCreateWriteStream).not.toHaveBeenCalled(); @@ -99,12 +101,14 @@ test('`append()` correctly formats records and pushes them to the file.', () => level: LogLevel.All, message: 'message-1', timestamp: new Date(), + pid: 5355, }, { context: 'context-2', level: LogLevel.Trace, message: 'message-2', timestamp: new Date(), + pid: 5355, }, { context: 'context-3', @@ -112,6 +116,7 @@ test('`append()` correctly formats records and pushes them to the file.', () => level: LogLevel.Fatal, message: 'message-3', timestamp: new Date(), + pid: 5355, }, ]; @@ -160,6 +165,7 @@ test('`dispose()` closes stream.', async () => { level: LogLevel.All, message: 'message-1', timestamp: new Date(), + pid: 5355, }); await appender.dispose(); diff --git a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap index d95c3893aa3d8..21cf4302c49dc 100644 --- a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`\`format()\` correctly formats record. 1`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-1\\",\\"error\\":{\\"message\\":\\"Some error message\\",\\"name\\":\\"Some error name\\",\\"stack\\":\\"Some error stack\\"},\\"level\\":\\"FATAL\\",\\"message\\":\\"message-1\\"}"`; +exports[`\`format()\` correctly formats record. 1`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-1\\",\\"error\\":{\\"message\\":\\"Some error message\\",\\"name\\":\\"Some error name\\",\\"stack\\":\\"Some error stack\\"},\\"level\\":\\"FATAL\\",\\"message\\":\\"message-1\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 2`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-2\\",\\"level\\":\\"ERROR\\",\\"message\\":\\"message-2\\"}"`; +exports[`\`format()\` correctly formats record. 2`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-2\\",\\"level\\":\\"ERROR\\",\\"message\\":\\"message-2\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 3`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-3\\",\\"level\\":\\"WARN\\",\\"message\\":\\"message-3\\"}"`; +exports[`\`format()\` correctly formats record. 3`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-3\\",\\"level\\":\\"WARN\\",\\"message\\":\\"message-3\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 4`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-4\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-4\\"}"`; +exports[`\`format()\` correctly formats record. 4`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-4\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-4\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 5`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-5\\",\\"level\\":\\"INFO\\",\\"message\\":\\"message-5\\"}"`; +exports[`\`format()\` correctly formats record. 5`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-5\\",\\"level\\":\\"INFO\\",\\"message\\":\\"message-5\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 6`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-6\\",\\"level\\":\\"TRACE\\",\\"message\\":\\"message-6\\"}"`; +exports[`\`format()\` correctly formats record. 6`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-6\\",\\"level\\":\\"TRACE\\",\\"message\\":\\"message-6\\",\\"pid\\":5355}"`; diff --git a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap index b727fc3c478ff..9ff4f7445d043 100644 --- a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap @@ -35,3 +35,15 @@ exports[`\`format()\` correctly formats record with highlighting. 4`] = `"[2012- exports[`\`format()\` correctly formats record with highlighting. 5`] = `"[2012-02-01T00:00:00.000Z][INFO ][context-5] message-5"`; exports[`\`format()\` correctly formats record with highlighting. 6`] = `"[2012-02-01T00:00:00.000Z][TRACE][context-6] message-6"`; + +exports[`allows specifying the PID in custom pattern 1`] = `"5355-context-1-Some error stack"`; + +exports[`allows specifying the PID in custom pattern 2`] = `"5355-context-2-message-2"`; + +exports[`allows specifying the PID in custom pattern 3`] = `"5355-context-3-message-3"`; + +exports[`allows specifying the PID in custom pattern 4`] = `"5355-context-4-message-4"`; + +exports[`allows specifying the PID in custom pattern 5`] = `"5355-context-5-message-5"`; + +exports[`allows specifying the PID in custom pattern 6`] = `"5355-context-6-message-6"`; diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index 49b8ddef07a63..2e4c5af80dd2e 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -32,36 +32,42 @@ const records: LogRecord[] = [ level: LogLevel.Fatal, message: 'message-1', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-2', level: LogLevel.Error, message: 'message-2', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-3', level: LogLevel.Warn, message: 'message-3', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-4', level: LogLevel.Debug, message: 'message-4', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-5', level: LogLevel.Info, message: 'message-5', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-6', level: LogLevel.Trace, message: 'message-6', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, ]; diff --git a/src/core/server/logging/layouts/json_layout.ts b/src/core/server/logging/layouts/json_layout.ts index 1dcc3fc3ebcf6..8e90b2f7eb782 100644 --- a/src/core/server/logging/layouts/json_layout.ts +++ b/src/core/server/logging/layouts/json_layout.ts @@ -58,6 +58,7 @@ export class JsonLayout implements Layout { level: record.level.id.toUpperCase(), message: record.message, meta: record.meta, + pid: record.pid, }); } } diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index ae8b39b9cc99a..23656c5d20510 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -33,36 +33,42 @@ const records: LogRecord[] = [ level: LogLevel.Fatal, message: 'message-1', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-2', level: LogLevel.Error, message: 'message-2', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-3', level: LogLevel.Warn, message: 'message-3', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-4', level: LogLevel.Debug, message: 'message-4', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-5', level: LogLevel.Info, message: 'message-5', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, { context: 'context-6', level: LogLevel.Trace, message: 'message-6', timestamp: new Date(Date.UTC(2012, 1, 1)), + pid: 5355, }, ]; @@ -119,3 +125,11 @@ test('`format()` correctly formats record with highlighting.', () => { expect(layout.format(record)).toMatchSnapshot(); } }); + +test('allows specifying the PID in custom pattern', () => { + const layout = new PatternLayout('{pid}-{context}-{message}'); + + for (const record of records) { + expect(layout.format(record)).toMatchSnapshot(); + } +}); diff --git a/src/core/server/logging/layouts/pattern_layout.ts b/src/core/server/logging/layouts/pattern_layout.ts index 8be47dbcdd095..64424c02268ff 100644 --- a/src/core/server/logging/layouts/pattern_layout.ts +++ b/src/core/server/logging/layouts/pattern_layout.ts @@ -32,6 +32,7 @@ const Parameters = Object.freeze({ Level: '{level}', Message: '{message}', Timestamp: '{timestamp}', + Pid: '{pid}', }); /** @@ -39,7 +40,7 @@ const Parameters = Object.freeze({ * with the actual data. */ const PATTERN_REGEX = new RegExp( - `${Parameters.Timestamp}|${Parameters.Level}|${Parameters.Context}|${Parameters.Message}`, + `${Parameters.Timestamp}|${Parameters.Level}|${Parameters.Context}|${Parameters.Message}|${Parameters.Pid}`, 'gi' ); @@ -103,6 +104,7 @@ export class PatternLayout implements Layout { [Parameters.Level, record.level.id.toUpperCase().padEnd(5)], [Parameters.Context, record.context], [Parameters.Message, message], + [Parameters.Pid, String(record.pid)], ]); if (this.highlight) { diff --git a/src/core/server/logging/log_record.ts b/src/core/server/logging/log_record.ts index e7f93f7fc3e14..6286d751bf41f 100644 --- a/src/core/server/logging/log_record.ts +++ b/src/core/server/logging/log_record.ts @@ -30,4 +30,5 @@ export interface LogRecord { message: string; error?: Error; meta?: { [name: string]: any }; + pid: number; } diff --git a/src/core/server/logging/logger.test.ts b/src/core/server/logging/logger.test.ts index 026e24fc5df54..1cc00a254300b 100644 --- a/src/core/server/logging/logger.test.ts +++ b/src/core/server/logging/logger.test.ts @@ -53,6 +53,7 @@ test('`trace()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-1', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -66,6 +67,7 @@ test('`trace()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-2', meta: { trace: true }, timestamp, + pid: expect.any(Number), }); } }); @@ -81,6 +83,7 @@ test('`debug()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-1', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -94,6 +97,7 @@ test('`debug()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-2', meta: { debug: true }, timestamp, + pid: expect.any(Number), }); } }); @@ -109,6 +113,7 @@ test('`info()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-1', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -122,6 +127,7 @@ test('`info()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-2', meta: { info: true }, timestamp, + pid: expect.any(Number), }); } }); @@ -137,6 +143,7 @@ test('`warn()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-1', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -151,6 +158,7 @@ test('`warn()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-2', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -164,6 +172,7 @@ test('`warn()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-3', meta: { warn: true }, timestamp, + pid: expect.any(Number), }); } }); @@ -179,6 +188,7 @@ test('`error()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-1', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -193,6 +203,7 @@ test('`error()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-2', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -206,6 +217,7 @@ test('`error()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-3', meta: { error: true }, timestamp, + pid: expect.any(Number), }); } }); @@ -221,6 +233,7 @@ test('`fatal()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-1', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -235,6 +248,7 @@ test('`fatal()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-2', meta: undefined, timestamp, + pid: expect.any(Number), }); } @@ -248,6 +262,7 @@ test('`fatal()` correctly forms `LogRecord` and passes it to all appenders.', () message: 'message-3', meta: { fatal: true }, timestamp, + pid: expect.any(Number), }); } }); @@ -258,6 +273,7 @@ test('`log()` just passes the record to all appenders.', () => { level: LogLevel.Info, message: 'message-1', timestamp, + pid: 5355, }; logger.log(record); @@ -307,6 +323,7 @@ test('logger with `All` level passes all records to appenders.', () => { level: LogLevel.Trace, message: 'trace-message', timestamp, + pid: expect.any(Number), }); } @@ -318,6 +335,7 @@ test('logger with `All` level passes all records to appenders.', () => { level: LogLevel.Debug, message: 'debug-message', timestamp, + pid: expect.any(Number), }); } @@ -329,6 +347,7 @@ test('logger with `All` level passes all records to appenders.', () => { level: LogLevel.Info, message: 'info-message', timestamp, + pid: expect.any(Number), }); } @@ -340,6 +359,7 @@ test('logger with `All` level passes all records to appenders.', () => { level: LogLevel.Warn, message: 'warn-message', timestamp, + pid: expect.any(Number), }); } @@ -351,6 +371,7 @@ test('logger with `All` level passes all records to appenders.', () => { level: LogLevel.Error, message: 'error-message', timestamp, + pid: expect.any(Number), }); } @@ -362,6 +383,7 @@ test('logger with `All` level passes all records to appenders.', () => { level: LogLevel.Fatal, message: 'fatal-message', timestamp, + pid: expect.any(Number), }); } }); @@ -385,6 +407,7 @@ test('passes log record to appenders only if log level is supported.', () => { level: LogLevel.Warn, message: 'warn-message', timestamp, + pid: expect.any(Number), }); } @@ -396,6 +419,7 @@ test('passes log record to appenders only if log level is supported.', () => { level: LogLevel.Error, message: 'error-message', timestamp, + pid: expect.any(Number), }); } @@ -407,6 +431,7 @@ test('passes log record to appenders only if log level is supported.', () => { level: LogLevel.Fatal, message: 'fatal-message', timestamp, + pid: expect.any(Number), }); } }); diff --git a/src/core/server/logging/logger.ts b/src/core/server/logging/logger.ts index ac79c1916c07b..285998c23832c 100644 --- a/src/core/server/logging/logger.ts +++ b/src/core/server/logging/logger.ts @@ -162,6 +162,7 @@ export class BaseLogger implements Logger { message: errorOrMessage.message, meta, timestamp: new Date(), + pid: process.pid, }; } @@ -171,6 +172,7 @@ export class BaseLogger implements Logger { message: errorOrMessage, meta, timestamp: new Date(), + pid: process.pid, }; } } diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts index c58103cca5f8d..51697fd15bebe 100644 --- a/src/core/server/logging/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -23,6 +23,8 @@ jest.mock('fs', () => ({ createWriteStream: jest.fn(() => ({ write: mockStreamWrite })), })); +const dynamicProps = { pid: expect.any(Number) }; + jest.mock('../../../legacy/server/logging/rotate', () => ({ setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})), })); @@ -58,7 +60,9 @@ test('uses default memory buffer logger until config is provided', () => { const anotherLogger = service.get('test', 'context2'); anotherLogger.fatal('fatal message', { some: 'value' }); - expect(bufferAppendSpy.mock.calls).toMatchSnapshot(); + expect(bufferAppendSpy).toHaveBeenCalledTimes(2); + expect(bufferAppendSpy.mock.calls[0][0]).toMatchSnapshot(dynamicProps); + expect(bufferAppendSpy.mock.calls[1][0]).toMatchSnapshot(dynamicProps); }); test('flushes memory buffer logger and switches to real logger once config is provided', () => { @@ -78,12 +82,15 @@ test('flushes memory buffer logger and switches to real logger once config is pr }) ); - expect(mockConsoleLog.mock.calls).toMatchSnapshot('buffered messages'); + expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot( + dynamicProps, + 'buffered messages' + ); mockConsoleLog.mockClear(); // Now message should go straight to thew newly configured appender, not buffered one. logger.info('some new info message'); - expect(mockConsoleLog.mock.calls).toMatchSnapshot('new messages'); + expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps, 'new messages'); expect(bufferAppendSpy).not.toHaveBeenCalled(); }); @@ -114,8 +121,12 @@ test('appends records via multiple appenders.', () => { ); // Now all logs should added to configured appenders. - expect(mockConsoleLog.mock.calls).toMatchSnapshot('console logs'); - expect(mockStreamWrite.mock.calls).toMatchSnapshot('file logs'); + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog.mock.calls[0][0]).toMatchSnapshot('console logs'); + + expect(mockStreamWrite).toHaveBeenCalledTimes(2); + expect(mockStreamWrite.mock.calls[0][0]).toMatchSnapshot('file logs'); + expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs'); }); test('uses `root` logger if context is not specified.', () => { @@ -163,5 +174,9 @@ test('asLoggerFactory() only allows to create new loggers.', () => { logger.fatal('buffered fatal message'); expect(Object.keys(service.asLoggerFactory())).toEqual(['get']); - expect(mockConsoleLog.mock.calls).toMatchSnapshot(); + + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + expect(JSON.parse(mockConsoleLog.mock.calls[0][0])).toMatchSnapshot(dynamicProps); + expect(JSON.parse(mockConsoleLog.mock.calls[1][0])).toMatchSnapshot(dynamicProps); + expect(JSON.parse(mockConsoleLog.mock.calls[2][0])).toMatchSnapshot(dynamicProps); }); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 97f836f8ef37d..9a7868d568ea0 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -38,6 +38,7 @@ export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; +export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { uuidServiceMock } from './uuid/uuid_service.mock'; @@ -112,12 +113,19 @@ function createCoreSetupMock() { const uiSettingsMock = { register: uiSettingsServiceMock.createSetupContract().register, }; + + const savedObjectsService = savedObjectsServiceMock.createSetupContract(); + const savedObjectMock: jest.Mocked = { + addClientWrapper: savedObjectsService.addClientWrapper, + setClientFactoryProvider: savedObjectsService.setClientFactoryProvider, + }; + const mock: CoreSetupMockType = { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetup(), http: httpMock, - savedObjects: savedObjectsServiceMock.createSetupContract(), + savedObjects: savedObjectMock, uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), getStartServices: jest @@ -145,7 +153,7 @@ function createInternalCoreSetupMock() { elasticsearch: elasticsearchServiceMock.createInternalSetup(), http: httpServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), - savedObjects: savedObjectsServiceMock.createSetupContract(), + savedObjects: savedObjectsServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), }; return setupDeps; @@ -154,7 +162,7 @@ function createInternalCoreSetupMock() { function createInternalCoreStartMock() { const startDeps: InternalCoreStart = { capabilities: capabilitiesServiceMock.createStartContract(), - savedObjects: savedObjectsServiceMock.createStartContract(), + savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), }; return startDeps; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 77300900e84f3..a7b555a9eba01 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -205,6 +205,7 @@ export function createPluginStartContext( getScopedClient: deps.savedObjects.getScopedClient, createInternalRepository: deps.savedObjects.createInternalRepository, createScopedRepository: deps.savedObjects.createScopedRepository, + createSerializer: deps.savedObjects.createSerializer, }, uiSettings: { asScopedToClient: deps.uiSettings.asScopedToClient, diff --git a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap new file mode 100644 index 0000000000000..7846e7f1a802a --- /dev/null +++ b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`convertLegacyTypes converts the legacy mappings using default values if no schemas are specified 1`] = ` +Array [ + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": undefined, + "mappings": Object { + "properties": Object { + "fieldA": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeA", + "namespaceAgnostic": false, + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": undefined, + "mappings": Object { + "properties": Object { + "fieldB": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeB", + "namespaceAgnostic": false, + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": undefined, + "mappings": Object { + "properties": Object { + "fieldC": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeC", + "namespaceAgnostic": false, + }, +] +`; + +exports[`convertLegacyTypes merges everything when all are present 1`] = ` +Array [ + Object { + "convertToAliasScript": undefined, + "hidden": true, + "indexPattern": "myIndex", + "mappings": Object { + "properties": Object { + "fieldA": Object { + "type": "text", + }, + }, + }, + "migrations": Object { + "1.0.0": [MockFunction], + "2.0.4": [MockFunction], + }, + "name": "typeA", + "namespaceAgnostic": true, + }, + Object { + "convertToAliasScript": "some alias script", + "hidden": false, + "indexPattern": undefined, + "mappings": Object { + "properties": Object { + "anotherFieldB": Object { + "type": "boolean", + }, + "fieldB": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeB", + "namespaceAgnostic": false, + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": undefined, + "mappings": Object { + "properties": Object { + "fieldC": Object { + "type": "text", + }, + }, + }, + "migrations": Object { + "1.5.3": [MockFunction], + }, + "name": "typeC", + "namespaceAgnostic": false, + }, +] +`; + +exports[`convertLegacyTypes merges the mappings and the schema to create the type when schema exists for the type 1`] = ` +Array [ + Object { + "convertToAliasScript": undefined, + "hidden": true, + "indexPattern": "fooBar", + "mappings": Object { + "properties": Object { + "fieldA": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeA", + "namespaceAgnostic": true, + }, + Object { + "convertToAliasScript": undefined, + "hidden": false, + "indexPattern": undefined, + "mappings": Object { + "properties": Object { + "fieldC": Object { + "type": "text", + }, + }, + }, + "migrations": Object {}, + "name": "typeC", + "namespaceAgnostic": false, + }, +] +`; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 181025d73817d..529ee9599f178 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -31,7 +31,12 @@ export { SavedObjectsExportResultDetails, } from './export'; -export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serialization'; +export { + SavedObjectsSerializer, + SavedObjectsRawDoc, + SavedObjectSanitizedDoc, + SavedObjectUnsanitizedDoc, +} from './serialization'; export { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; @@ -50,4 +55,18 @@ export { SavedObjectsDeleteByNamespaceOptions, } from './service/lib/repository'; +export { + SavedObjectsCoreFieldMapping, + SavedObjectsComplexFieldMapping, + SavedObjectsFieldMapping, + SavedObjectsMappingProperties, + SavedObjectsTypeMappingDefinition, + SavedObjectsTypeMappingDefinitions, +} from './mappings'; + +export { SavedObjectMigrationMap, SavedObjectMigrationFn } from './migrations'; + +export { SavedObjectsType } from './types'; + export { config } from './saved_objects_config'; +export { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; diff --git a/src/core/server/saved_objects/mappings/index.ts b/src/core/server/saved_objects/mappings/index.ts index 15b0736ca5f1f..e1d718ee454ce 100644 --- a/src/core/server/saved_objects/mappings/index.ts +++ b/src/core/server/saved_objects/mappings/index.ts @@ -18,9 +18,12 @@ */ export { getTypes, getProperty, getRootProperties, getRootPropertiesObjects } from './lib'; export { - FieldMapping, - MappingMeta, - MappingProperties, + SavedObjectsComplexFieldMapping, + SavedObjectsCoreFieldMapping, + SavedObjectsTypeMappingDefinition, + SavedObjectsTypeMappingDefinitions, + SavedObjectsMappingProperties, + SavedObjectsFieldMapping, + IndexMappingMeta, IndexMapping, - SavedObjectsMapping, } from './types'; diff --git a/src/core/server/saved_objects/mappings/lib/get_property.test.ts b/src/core/server/saved_objects/mappings/lib/get_property.test.ts index a85697ddd08b8..a271cf3826e5d 100644 --- a/src/core/server/saved_objects/mappings/lib/get_property.test.ts +++ b/src/core/server/saved_objects/mappings/lib/get_property.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { FieldMapping, IndexMapping } from '../types'; +import { SavedObjectsFieldMapping, IndexMapping } from '../types'; import { getProperty } from './get_property'; const MAPPINGS = { @@ -47,7 +47,7 @@ const MAPPINGS = { }, }; -function runTest(key: string | string[], mapping: IndexMapping | FieldMapping) { +function runTest(key: string | string[], mapping: IndexMapping | SavedObjectsFieldMapping) { expect(typeof key === 'string' || Array.isArray(key)).toBeTruthy(); expect(typeof mapping).toBe('object'); diff --git a/src/core/server/saved_objects/mappings/lib/get_property.ts b/src/core/server/saved_objects/mappings/lib/get_property.ts index 4e9ff10fc5d47..a31c9fe0c3ba1 100644 --- a/src/core/server/saved_objects/mappings/lib/get_property.ts +++ b/src/core/server/saved_objects/mappings/lib/get_property.ts @@ -18,15 +18,15 @@ */ import toPath from 'lodash/internal/toPath'; -import { CoreFieldMapping, FieldMapping, IndexMapping } from '../types'; +import { SavedObjectsCoreFieldMapping, SavedObjectsFieldMapping, IndexMapping } from '../types'; function getPropertyMappingFromObjectMapping( - mapping: IndexMapping | FieldMapping, + mapping: IndexMapping | SavedObjectsFieldMapping, path: string[] -): FieldMapping | undefined { +): SavedObjectsFieldMapping | undefined { const props = (mapping && (mapping as IndexMapping).properties) || - (mapping && (mapping as CoreFieldMapping).fields); + (mapping && (mapping as SavedObjectsCoreFieldMapping).fields); if (!props) { return undefined; @@ -39,6 +39,9 @@ function getPropertyMappingFromObjectMapping( } } -export function getProperty(mappings: IndexMapping | FieldMapping, path: string | string[]) { +export function getProperty( + mappings: IndexMapping | SavedObjectsFieldMapping, + path: string | string[] +) { return getPropertyMappingFromObjectMapping(mappings, toPath(path)); } diff --git a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts index 61e4d752445c4..81ba1d8235561 100644 --- a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts +++ b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts @@ -17,7 +17,11 @@ * under the License. */ -import { ComplexFieldMapping, IndexMapping, MappingProperties } from '../types'; +import { + SavedObjectsComplexFieldMapping, + IndexMapping, + SavedObjectsMappingProperties, +} from '../types'; import { getRootProperties } from './get_root_properties'; /** @@ -43,10 +47,10 @@ export function getRootPropertiesObjects(mappings: IndexMapping) { // we consider the existence of the properties or type of object to designate that this is an object datatype if ( !blacklist.includes(key) && - ((value as ComplexFieldMapping).properties || value.type === 'object') + ((value as SavedObjectsComplexFieldMapping).properties || value.type === 'object') ) { acc[key] = value; } return acc; - }, {} as MappingProperties); + }, {} as SavedObjectsMappingProperties); } diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index 8bb1a69d2eb13..578fdcea3718e 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -17,47 +17,133 @@ * under the License. */ -// FieldMapping isn't 1:1 with the options available, -// modify as needed. -export interface CoreFieldMapping { - type: string; - fields?: { - [subfield: string]: { - type: string; - }; - }; +/** + * Describe a saved object type mapping. + * + * @example + * ```ts + * const typeDefinition: SavedObjectsTypeMappingDefinition = { + * properties: { + * enabled: { + * type: "boolean" + * }, + * sendUsageFrom: { + * ignore_above: 256, + * type: "keyword" + * }, + * lastReported: { + * type: "date" + * }, + * lastVersionChecked: { + * ignore_above: 256, + * type: "keyword" + * }, + * } + * } + * ``` + * + * @public + */ +export interface SavedObjectsTypeMappingDefinition { + properties: SavedObjectsMappingProperties; } -// FieldMapping isn't 1:1 with the options available, -// modify as needed. -export interface ComplexFieldMapping { - dynamic?: string; - type?: string; - properties: MappingProperties; -} +/** + * A map of {@link SavedObjectsTypeMappingDefinition | saved object type mappings} + * + * @example + * ```ts + * const mappings: SavedObjectsTypeMappingDefinitions = { + * someType: { + * properties: { + * enabled: { + * type: "boolean" + * }, + * field: { + * type: "keyword" + * }, + * }, + * }, + * anotherType: { + * properties: { + * enabled: { + * type: "boolean" + * }, + * lastReported: { + * type: "date" + * }, + * }, + * }, -export type FieldMapping = CoreFieldMapping | ComplexFieldMapping; + * } + * ``` + * @remark This is the format for the legacy `mappings.json` savedObject mapping file. + * + * @internal + */ +export interface SavedObjectsTypeMappingDefinitions { + [type: string]: SavedObjectsTypeMappingDefinition; +} -export interface MappingProperties { - [field: string]: FieldMapping; +/** + * Describe the fields of a {@link SavedObjectsTypeMappingDefinition | saved object type}. + * + * @public + */ +export interface SavedObjectsMappingProperties { + [field: string]: SavedObjectsFieldMapping; } -export interface SavedObjectsMapping { - pluginId: string; - properties: MappingProperties; +/** + * Describe a {@link SavedObjectsTypeMappingDefinition | saved object type mapping} field. + * + * Please refer to {@link https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html | elasticsearch documentation} + * For the mapping documentation + * + * @public + */ +export type SavedObjectsFieldMapping = + | SavedObjectsCoreFieldMapping + | SavedObjectsComplexFieldMapping; + +/** @internal */ +export interface IndexMapping { + dynamic?: string; + properties: SavedObjectsMappingProperties; + _meta?: IndexMappingMeta; } -export interface MappingMeta { +/** @internal */ +export interface IndexMappingMeta { // A dictionary of key -> md5 hash (e.g. 'dashboard': '24234qdfa3aefa3wa') // with each key being a root-level mapping property, and each value being // the md5 hash of that mapping's value when the index was created. migrationMappingPropertyHashes?: { [k: string]: string }; } -// IndexMapping isn't 1:1 with the options available, -// modify as needed. -export interface IndexMapping { +/** + * See {@link SavedObjectsFieldMapping} for documentation. + * + * @public + */ +export interface SavedObjectsCoreFieldMapping { + type: string; + index?: boolean; + enabled?: boolean; + fields?: { + [subfield: string]: { + type: string; + }; + }; +} + +/** + * See {@link SavedObjectsFieldMapping} for documentation. + * + * @public + */ +export interface SavedObjectsComplexFieldMapping { dynamic?: string; - properties: MappingProperties; - _meta?: MappingMeta; + type?: string; + properties: SavedObjectsMappingProperties; } diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts index 71f589f24369a..821a10353f8ec 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.test.ts @@ -27,21 +27,19 @@ describe('buildActiveMappings', () => { bbb: { type: 'long' }, }; - expect(buildActiveMappings({ properties })).toMatchSnapshot(); + expect(buildActiveMappings(properties)).toMatchSnapshot(); }); test('disallows duplicate mappings', () => { const properties = { type: { type: 'long' } }; - expect(() => buildActiveMappings({ properties })).toThrow( - /Cannot redefine core mapping \"type\"/ - ); + expect(() => buildActiveMappings(properties)).toThrow(/Cannot redefine core mapping \"type\"/); }); test('disallows mappings with leading underscore', () => { const properties = { _hm: { type: 'keyword' } }; - expect(() => buildActiveMappings({ properties })).toThrow( + expect(() => buildActiveMappings(properties)).toThrow( /Invalid mapping \"_hm\"\. Mappings cannot start with _/ ); }); @@ -53,7 +51,7 @@ describe('buildActiveMappings', () => { ccc: { fields: { b: { type: 'text' }, a: { type: 'text' } }, type: 'keyword' }, }; - const mappings = buildActiveMappings({ properties }); + const mappings = buildActiveMappings(properties); const hashes = mappings._meta!.migrationMappingPropertyHashes!; expect(hashes.aaa).toBeDefined(); diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index 2cf640cceea83..3afe8aae119d9 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -22,31 +22,31 @@ */ import crypto from 'crypto'; -import _ from 'lodash'; -import { IndexMapping, MappingProperties } from './../../mappings'; +import { cloneDeep, mapValues } from 'lodash'; +import { + IndexMapping, + SavedObjectsMappingProperties, + SavedObjectsTypeMappingDefinitions, +} from './../../mappings'; /** * Creates an index mapping with the core properties required by saved object * indices, as well as the specified additional properties. * - * @param {Opts} opts - * @prop {MappingDefinition} properties - The mapping's properties - * @returns {IndexMapping} + * @param typeDefinitions - the type definitions to build mapping from. */ -export function buildActiveMappings({ - properties, -}: { - properties: MappingProperties; -}): IndexMapping { +export function buildActiveMappings( + typeDefinitions: SavedObjectsTypeMappingDefinitions | SavedObjectsMappingProperties +): IndexMapping { const mapping = defaultMapping(); - properties = validateAndMerge(mapping.properties, properties); + const mergedProperties = validateAndMerge(mapping.properties, typeDefinitions); - return _.cloneDeep({ + return cloneDeep({ ...mapping, - properties, + properties: mergedProperties, _meta: { - migrationMappingPropertyHashes: md5Values(properties), + migrationMappingPropertyHashes: md5Values(mergedProperties), }, }); } @@ -113,7 +113,7 @@ function canonicalStringify(obj: any): string { // Convert an object's values to md5 hash strings function md5Values(obj: any) { - return _.mapValues(obj, md5Object); + return mapValues(obj, md5Object); } // If something exists in actual, but is missing in expected, we don't @@ -171,12 +171,14 @@ function defaultMapping(): IndexMapping { }; } -function validateAndMerge(dest: MappingProperties, source: MappingProperties) { +function validateAndMerge( + dest: SavedObjectsMappingProperties, + source: SavedObjectsTypeMappingDefinitions | SavedObjectsMappingProperties +) { Object.keys(source).forEach(k => { if (k.startsWith('_')) { throw new Error(`Invalid mapping "${k}". Mappings cannot start with _.`); } - if (dest.hasOwnProperty(k)) { throw new Error(`Cannot redefine core mapping "${k}".`); } diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts index bdc96323238c0..44add4e977006 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.test.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.test.ts @@ -18,20 +18,30 @@ */ import { createIndexMap } from './build_index_map'; -import { ObjectToConfigAdapter } from '../../../config'; -import { SavedObjectsSchema } from '../../schema'; -import { LegacyConfig } from '../../../legacy'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { SavedObjectsType } from '../../types'; -const config = (new ObjectToConfigAdapter({}) as unknown) as LegacyConfig; +const createRegistry = (...types: Array>) => { + const registry = new SavedObjectTypeRegistry(); + types.forEach(type => + registry.registerType({ + name: 'unknown', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + migrations: {}, + ...type, + }) + ); + return registry; +}; test('mappings without index pattern goes to default index', () => { const result = createIndexMap({ - config, kibanaIndexName: '.kibana', - schema: new SavedObjectsSchema({ - type1: { - isNamespaceAgnostic: false, - }, + registry: createRegistry({ + name: 'type1', + namespaceAgnostic: false, }), indexMap: { type1: { @@ -60,13 +70,11 @@ test('mappings without index pattern goes to default index', () => { test(`mappings with custom index pattern doesn't go to default index`, () => { const result = createIndexMap({ - config, kibanaIndexName: '.kibana', - schema: new SavedObjectsSchema({ - type1: { - isNamespaceAgnostic: false, - indexPattern: '.other_kibana', - }, + registry: createRegistry({ + name: 'type1', + namespaceAgnostic: false, + indexPattern: '.other_kibana', }), indexMap: { type1: { @@ -95,14 +103,12 @@ test(`mappings with custom index pattern doesn't go to default index`, () => { test('creating a script gets added to the index pattern', () => { const result = createIndexMap({ - config, kibanaIndexName: '.kibana', - schema: new SavedObjectsSchema({ - type1: { - isNamespaceAgnostic: false, - indexPattern: '.other_kibana', - convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, - }, + registry: createRegistry({ + name: 'type1', + namespaceAgnostic: false, + indexPattern: '.other_kibana', + convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }), indexMap: { type1: { @@ -132,16 +138,19 @@ test('creating a script gets added to the index pattern', () => { test('throws when two scripts are defined for an index pattern', () => { const defaultIndex = '.kibana'; - const schema = new SavedObjectsSchema({ - type1: { - isNamespaceAgnostic: false, + const registry = createRegistry( + { + name: 'type1', + namespaceAgnostic: false, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, }, - type2: { - isNamespaceAgnostic: false, + { + name: 'type2', + namespaceAgnostic: false, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, - }, - }); + } + ); + const indexMap = { type1: { properties: { @@ -160,9 +169,8 @@ test('throws when two scripts are defined for an index pattern', () => { }; expect(() => createIndexMap({ - config, kibanaIndexName: defaultIndex, - schema, + registry, indexMap, }) ).toThrowErrorMatchingInlineSnapshot( diff --git a/src/core/server/saved_objects/migrations/core/build_index_map.ts b/src/core/server/saved_objects/migrations/core/build_index_map.ts index 914447563cc77..8f7fe2f8eac5b 100644 --- a/src/core/server/saved_objects/migrations/core/build_index_map.ts +++ b/src/core/server/saved_objects/migrations/core/build_index_map.ts @@ -17,39 +17,32 @@ * under the License. */ -import { MappingProperties } from '../../mappings'; -import { SavedObjectsSchema } from '../../schema'; -import { LegacyConfig } from '../../../legacy'; +import { SavedObjectsTypeMappingDefinitions } from '../../mappings'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; export interface CreateIndexMapOptions { - config: LegacyConfig; kibanaIndexName: string; - schema: SavedObjectsSchema; - indexMap: MappingProperties; + registry: ISavedObjectTypeRegistry; + indexMap: SavedObjectsTypeMappingDefinitions; } export interface IndexMap { [index: string]: { - typeMappings: MappingProperties; + typeMappings: SavedObjectsTypeMappingDefinitions; script?: string; }; } /* - * This file contains logic to convert savedObjectSchemas into a dictonary of indexes and documents + * This file contains logic to convert savedObjectSchemas into a dictionary of indexes and documents */ -export function createIndexMap({ - /** @deprecated Remove once savedObjectsSchemas are exposed from Core */ - config, - kibanaIndexName, - schema, - indexMap, -}: CreateIndexMapOptions) { +export function createIndexMap({ kibanaIndexName, registry, indexMap }: CreateIndexMapOptions) { const map: IndexMap = {}; Object.keys(indexMap).forEach(type => { - const script = schema.getConvertToAliasScript(type); + const typeDef = registry.getType(type); + const script = typeDef?.convertToAliasScript; // Defaults to kibanaIndexName if indexPattern isn't defined - const indexPattern = schema.getIndexForType(config, type) || kibanaIndexName; + const indexPattern = typeDef?.indexPattern || kibanaIndexName; if (!map.hasOwnProperty(indexPattern as string)) { map[indexPattern] = { typeMappings: {} }; } 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 38496a3503833..0e3a4780e12b6 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 @@ -18,41 +18,49 @@ */ import _ from 'lodash'; -import { RawSavedObjectDoc } from '../../serialization'; +import { SavedObjectUnsanitizedDoc } from '../../serialization'; import { DocumentMigrator } from './document_migrator'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; +import { SavedObjectsType } from '../../types'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; const mockLoggerFactory = loggingServiceMock.create(); const mockLogger = mockLoggerFactory.get('mock logger'); +const createRegistry = (...types: Array>) => { + const registry = new SavedObjectTypeRegistry(); + types.forEach(type => + registry.registerType({ + name: 'unknown', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + migrations: {}, + ...type, + }) + ); + return registry; +}; + describe('DocumentMigrator', () => { function testOpts() { return { kibanaVersion: '25.2.3', - migrations: {}, + typeRegistry: createRegistry(), validateDoc: _.noop, log: mockLogger, }; } - it('validates the migration definition', () => { - const invalidDefinition: any = { - kibanaVersion: '3.2.3', - migrations: 'hello', - validateDoc: _.noop, - }; - expect(() => new DocumentMigrator(invalidDefinition)).toThrow( - /Migration definition should be an object/i - ); - }); - it('validates individual migration definitions', () => { - const invalidDefinition: any = { + const invalidDefinition = { kibanaVersion: '3.2.3', - migrations: { - foo: _.noop, - }, + typeRegistry: createRegistry({ + name: 'foo', + migrations: _.noop as any, + }), validateDoc: _.noop, + log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrow( /Migration for type foo should be an object/i @@ -60,14 +68,16 @@ describe('DocumentMigrator', () => { }); it('validates individual migration semvers', () => { - const invalidDefinition: any = { + const invalidDefinition = { kibanaVersion: '3.2.3', - migrations: { - foo: { - bar: _.noop, + typeRegistry: createRegistry({ + name: 'foo', + migrations: { + bar: doc => doc, }, - }, + }), validateDoc: _.noop, + log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrow( /Expected all properties to be semvers/i @@ -75,14 +85,16 @@ describe('DocumentMigrator', () => { }); it('validates the migration function', () => { - const invalidDefinition: any = { + const invalidDefinition = { kibanaVersion: '3.2.3', - migrations: { - foo: { - '1.2.3': 23, + typeRegistry: createRegistry({ + name: 'foo', + migrations: { + '1.2.3': 23 as any, }, - }, + }), validateDoc: _.noop, + log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrow( /expected a function, but got 23/i @@ -92,11 +104,12 @@ describe('DocumentMigrator', () => { it('migrates type and attributes', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - user: { + typeRegistry: createRegistry({ + name: 'user', + migrations: { '1.2.3': setAttr('attributes.name', 'Chris'), }, - }, + }), }); const actual = migrator.migrate({ id: 'me', @@ -115,14 +128,15 @@ describe('DocumentMigrator', () => { it(`doesn't mutate the original document`, () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - user: { - '1.2.3': (doc: RawSavedObjectDoc) => { + typeRegistry: createRegistry({ + name: 'user', + migrations: { + '1.2.3': doc => { _.set(doc, 'attributes.name', 'Mike'); return doc; }, }, - }, + }), }); const originalDoc = { id: 'me', @@ -138,11 +152,12 @@ describe('DocumentMigrator', () => { it('migrates meta properties', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - acl: { + typeRegistry: createRegistry({ + name: 'acl', + migrations: { '2.3.5': setAttr('acl', 'admins-only,sucka!'), }, - }, + }), }); const actual = migrator.migrate({ id: 'me', @@ -163,11 +178,26 @@ describe('DocumentMigrator', () => { it('does not apply migrations to unrelated docs', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - aaa: { '1.0.0': setAttr('aaa', 'A') }, - bbb: { '1.0.0': setAttr('bbb', 'B') }, - ccc: { '1.0.0': setAttr('ccc', 'C') }, - }, + typeRegistry: createRegistry( + { + name: 'aaa', + migrations: { + '1.0.0': setAttr('aaa', 'A'), + }, + }, + { + name: 'bbb', + migrations: { + '1.0.0': setAttr('bbb', 'B'), + }, + }, + { + name: 'ccc', + migrations: { + '1.0.0': setAttr('ccc', 'C'), + }, + } + ), }); const actual = migrator.migrate({ id: 'me', @@ -185,11 +215,26 @@ describe('DocumentMigrator', () => { it('assumes documents w/ undefined migrationVersion are up to date', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - user: { '1.0.0': setAttr('aaa', 'A') }, - bbb: { '2.3.4': setAttr('bbb', 'B') }, - ccc: { '1.0.0': setAttr('ccc', 'C') }, - }, + typeRegistry: createRegistry( + { + name: 'user', + migrations: { + '1.0.0': setAttr('aaa', 'A'), + }, + }, + { + name: 'bbb', + migrations: { + '2.3.4': setAttr('bbb', 'B'), + }, + }, + { + name: 'ccc', + migrations: { + '1.0.0': setAttr('ccc', 'C'), + }, + } + ), }); const actual = migrator.migrate({ id: 'me', @@ -212,13 +257,14 @@ describe('DocumentMigrator', () => { it('only applies migrations that are more recent than the doc', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - dog: { + typeRegistry: createRegistry({ + name: 'dog', + migrations: { '1.2.3': setAttr('attributes.a', 'A'), '1.2.4': setAttr('attributes.b', 'B'), '2.0.1': setAttr('attributes.c', 'C'), }, - }, + }), }); const actual = migrator.migrate({ id: 'smelly', @@ -254,11 +300,12 @@ describe('DocumentMigrator', () => { it('rejects docs that belong to a newer plugin', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - dawg: { + typeRegistry: createRegistry({ + name: 'dawg', + migrations: { '1.2.3': setAttr('attributes.a', 'A'), }, - }, + }), }); expect(() => migrator.migrate({ @@ -276,13 +323,14 @@ describe('DocumentMigrator', () => { let count = 0; const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - dog: { + typeRegistry: createRegistry({ + name: 'dog', + migrations: { '2.2.4': setAttr('attributes.b', () => ++count), '10.0.1': setAttr('attributes.c', () => ++count), '1.2.3': setAttr('attributes.a', () => ++count), }, - }, + }), }); const actual = migrator.migrate({ id: 'smelly', @@ -301,14 +349,20 @@ describe('DocumentMigrator', () => { it('allows props to be added', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - animal: { - '1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`), - }, - dog: { - '2.2.4': setAttr('animal', 'Doggie'), + typeRegistry: createRegistry( + { + name: 'animal', + migrations: { + '1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`), + }, }, - }, + { + name: 'dog', + migrations: { + '2.2.4': setAttr('animal', 'Doggie'), + }, + } + ), }); const actual = migrator.migrate({ id: 'smelly', @@ -328,16 +382,22 @@ describe('DocumentMigrator', () => { it('allows props to be renamed', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - animal: { - '1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`), - '3.2.1': renameAttr('animal', 'dawg'), - }, - dawg: { - '2.2.4': renameAttr('dawg', 'animal'), - '3.2.0': setAttr('dawg', (name: string) => `Dawg3.x: ${name}`), + typeRegistry: createRegistry( + { + name: 'animal', + migrations: { + '1.0.0': setAttr('animal', (name: string) => `Animal: ${name}`), + '3.2.1': renameAttr('animal', 'dawg'), + }, }, - }, + { + name: 'dawg', + migrations: { + '2.2.4': renameAttr('dawg', 'animal'), + '3.2.0': setAttr('dawg', (name: string) => `Dawg3.x: ${name}`), + }, + } + ), }); const actual = migrator.migrate({ id: 'smelly', @@ -358,14 +418,20 @@ describe('DocumentMigrator', () => { it('allows changing type', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - cat: { - '1.0.0': setAttr('attributes.name', (name: string) => `Kitty ${name}`), - }, - dog: { - '2.2.4': setAttr('type', 'cat'), + typeRegistry: createRegistry( + { + name: 'cat', + migrations: { + '1.0.0': setAttr('attributes.name', (name: string) => `Kitty ${name}`), + }, }, - }, + { + name: 'dog', + migrations: { + '2.2.4': setAttr('type', 'cat'), + }, + } + ), }); const actual = migrator.migrate({ id: 'smelly', @@ -384,11 +450,12 @@ describe('DocumentMigrator', () => { it('disallows updating a migrationVersion prop to a lower version', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - cat: { + typeRegistry: createRegistry({ + name: 'cat', + migrations: { '1.0.0': setAttr('migrationVersion.foo', '3.2.1'), }, - }, + }), }); expect(() => @@ -406,11 +473,12 @@ describe('DocumentMigrator', () => { it('disallows removing a migrationVersion prop', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - cat: { + typeRegistry: createRegistry({ + name: 'cat', + migrations: { '1.0.0': setAttr('migrationVersion', {}), }, - }, + }), }); expect(() => migrator.migrate({ @@ -427,8 +495,9 @@ describe('DocumentMigrator', () => { it('allows updating a migrationVersion prop to a later version', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - cat: { + typeRegistry: createRegistry({ + name: 'cat', + migrations: { '1.0.0': setAttr('migrationVersion.cat', '2.9.1'), '2.0.0': () => { throw new Error('POW!'); @@ -438,7 +507,7 @@ describe('DocumentMigrator', () => { }, '3.0.0': setAttr('attributes.name', 'Shiny'), }, - }, + }), }); const actual = migrator.migrate({ id: 'smelly', @@ -457,11 +526,12 @@ describe('DocumentMigrator', () => { it('allows adding props to migrationVersion', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - cat: { + typeRegistry: createRegistry({ + name: 'cat', + migrations: { '1.0.0': setAttr('migrationVersion.foo', '5.6.7'), }, - }, + }), }); const actual = migrator.migrate({ id: 'smelly', @@ -481,13 +551,14 @@ describe('DocumentMigrator', () => { const log = mockLogger; const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - dog: { + typeRegistry: createRegistry({ + name: 'dog', + migrations: { '1.2.3': () => { throw new Error('Dang diggity!'); }, }, - }, + }), log, }); const failedDoc = { @@ -511,15 +582,16 @@ describe('DocumentMigrator', () => { const logTestMsg = '...said the joker to the thief'; const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - dog: { + typeRegistry: createRegistry({ + name: 'dog', + migrations: { '1.2.3': (doc, log) => { log.info(logTestMsg); log.warning(logTestMsg); return doc; }, }, - }, + }), log: mockLogger, }); const doc = { @@ -536,17 +608,23 @@ describe('DocumentMigrator', () => { test('extracts the latest migration version info', () => { const { migrationVersion } = new DocumentMigrator({ ...testOpts(), - migrations: { - aaa: { - '1.2.3': (doc: RawSavedObjectDoc) => doc, - '10.4.0': (doc: RawSavedObjectDoc) => doc, - '2.2.1': (doc: RawSavedObjectDoc) => doc, - }, - bbb: { - '3.2.3': (doc: RawSavedObjectDoc) => doc, - '2.0.0': (doc: RawSavedObjectDoc) => doc, + typeRegistry: createRegistry( + { + name: 'aaa', + migrations: { + '1.2.3': (doc: SavedObjectUnsanitizedDoc) => doc, + '10.4.0': (doc: SavedObjectUnsanitizedDoc) => doc, + '2.2.1': (doc: SavedObjectUnsanitizedDoc) => doc, + }, }, - }, + { + name: 'bbb', + migrations: { + '3.2.3': (doc: SavedObjectUnsanitizedDoc) => doc, + '2.0.0': (doc: SavedObjectUnsanitizedDoc) => doc, + }, + } + ), }); expect(migrationVersion).toEqual({ @@ -558,11 +636,12 @@ describe('DocumentMigrator', () => { test('fails if the validate doc throws', () => { const migrator = new DocumentMigrator({ ...testOpts(), - migrations: { - aaa: { + typeRegistry: createRegistry({ + name: 'aaa', + migrations: { '2.3.4': d => _.set(d, 'attributes.counter', 42), }, - }, + }), validateDoc: d => { if ((d.attributes as any).counter === 42) { throw new Error('Meaningful!'); @@ -577,11 +656,15 @@ describe('DocumentMigrator', () => { }); function renameAttr(path: string, newPath: string) { - return (doc: RawSavedObjectDoc) => - _.omit(_.set(doc, newPath, _.get(doc, path)) as {}, path) as RawSavedObjectDoc; + return (doc: SavedObjectUnsanitizedDoc) => + _.omit(_.set(doc, newPath, _.get(doc, path)) as {}, path) as SavedObjectUnsanitizedDoc; } function setAttr(path: string, value: any) { - return (doc: RawSavedObjectDoc) => - _.set(doc, path, _.isFunction(value) ? value(_.get(doc, path)) : value) as RawSavedObjectDoc; + return (doc: SavedObjectUnsanitizedDoc) => + _.set( + doc, + path, + _.isFunction(value) ? value(_.get(doc, path)) : value + ) as SavedObjectUnsanitizedDoc; } 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 563d978dcc1f1..b5019b2874bec 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -65,23 +65,19 @@ import _ from 'lodash'; import cloneDeep from 'lodash.clonedeep'; import Semver from 'semver'; import { Logger } from '../../../logging'; -import { RawSavedObjectDoc } from '../../serialization'; +import { SavedObjectUnsanitizedDoc } from '../../serialization'; import { SavedObjectsMigrationVersion } from '../../types'; -import { MigrationLogger, SavedObjectsMigrationLogger } from './migration_logger'; +import { MigrationLogger } from './migration_logger'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { SavedObjectMigrationFn } from '../types'; -export type TransformFn = (doc: RawSavedObjectDoc) => RawSavedObjectDoc; +export type TransformFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; -type MigrationFn = (doc: RawSavedObjectDoc, log: SavedObjectsMigrationLogger) => RawSavedObjectDoc; +type ValidateDoc = (doc: SavedObjectUnsanitizedDoc) => void; -type ValidateDoc = (doc: RawSavedObjectDoc) => void; - -export interface MigrationDefinition { - [type: string]: { [version: string]: MigrationFn }; -} - -interface Opts { +interface DocumentMigratorOptions { kibanaVersion: string; - migrations: MigrationDefinition; + typeRegistry: ISavedObjectTypeRegistry; validateDoc: ValidateDoc; log: Logger; } @@ -114,22 +110,22 @@ export class DocumentMigrator implements VersionedTransformer { /** * Creates an instance of DocumentMigrator. * - * @param {Opts} opts + * @param {DocumentMigratorOptions} opts * @prop {string} kibanaVersion - The current version of Kibana - * @prop {MigrationDefinition} migrations - The migrations that will be used to migrate documents + * @prop {SavedObjectTypeRegistry} typeRegistry - The type registry to get type migrations from * @prop {ValidateDoc} validateDoc - A function which, given a document throws an error if it is * not up to date. This is used to ensure we don't let unmigrated documents slip through. * @prop {Logger} log - The migration logger * @memberof DocumentMigrator */ - constructor(opts: Opts) { - validateMigrationDefinition(opts.migrations); + constructor({ typeRegistry, kibanaVersion, log, validateDoc }: DocumentMigratorOptions) { + validateMigrationDefinition(typeRegistry); - this.migrations = buildActiveMigrations(opts.migrations, opts.log); + this.migrations = buildActiveMigrations(typeRegistry, log); this.transformDoc = buildDocumentTransform({ - kibanaVersion: opts.kibanaVersion, + kibanaVersion, migrations: this.migrations, - validateDoc: opts.validateDoc, + validateDoc, }); } @@ -147,11 +143,11 @@ export class DocumentMigrator implements VersionedTransformer { /** * Migrates a document to the latest version. * - * @param {RawSavedObjectDoc} doc - * @returns {RawSavedObjectDoc} + * @param {SavedObjectUnsanitizedDoc} doc + * @returns {SavedObjectUnsanitizedDoc} * @memberof DocumentMigrator */ - public migrate = (doc: RawSavedObjectDoc): RawSavedObjectDoc => { + public migrate = (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { // Clone the document to prevent accidental mutations on the original data // Ex: Importing sample data that is cached at import level, migrations would // execute on mutated data the second time. @@ -166,7 +162,7 @@ export class DocumentMigrator implements VersionedTransformer { * language. So, this is just to provide a little developer-friendly error messaging. Joi was * giving weird errors, so we're just doing manual validation. */ -function validateMigrationDefinition(migrations: MigrationDefinition) { +function validateMigrationDefinition(registry: ISavedObjectTypeRegistry) { function assertObject(obj: any, prefix: string) { if (!obj || typeof obj !== 'object') { throw new Error(`${prefix} Got ${obj}.`); @@ -187,17 +183,17 @@ function validateMigrationDefinition(migrations: MigrationDefinition) { } } - assertObject(migrations, 'Migration definition should be an object.'); - - Object.entries(migrations).forEach(([type, versions]: any) => { - assertObject( - versions, - `Migration for type ${type} should be an object like { '2.0.0': (doc) => doc }.` - ); - Object.entries(versions).forEach(([version, fn]) => { - assertValidSemver(version, type); - assertValidTransform(fn, version, type); - }); + registry.getAllTypes().forEach(type => { + if (type.migrations) { + assertObject( + type.migrations, + `Migration for type ${type.name} should be an object like { '2.0.0': (doc) => doc }.` + ); + Object.entries(type.migrations).forEach(([version, fn]) => { + assertValidSemver(version, type.name); + assertValidTransform(fn, version, type.name); + }); + } }); } @@ -207,19 +203,28 @@ function validateMigrationDefinition(migrations: MigrationDefinition) { * From: { type: { version: fn } } * To: { type: { latestVersion: string, transforms: [{ version: string, transform: fn }] } } */ -function buildActiveMigrations(migrations: MigrationDefinition, log: Logger): ActiveMigrations { - return _.mapValues(migrations, (versions, prop) => { - const transforms = Object.entries(versions) - .map(([version, transform]) => ({ - version, - transform: wrapWithTry(version, prop!, transform, log), - })) - .sort((a, b) => Semver.compare(a.version, b.version)); - return { - latestVersion: _.last(transforms).version, - transforms, - }; - }); +function buildActiveMigrations( + typeRegistry: ISavedObjectTypeRegistry, + log: Logger +): ActiveMigrations { + return typeRegistry + .getAllTypes() + .filter(type => type.migrations && Object.keys(type.migrations).length > 0) + .reduce((migrations, type) => { + const transforms = Object.entries(type.migrations!) + .map(([version, transform]) => ({ + version, + transform: wrapWithTry(version, type.name, transform, log), + })) + .sort((a, b) => Semver.compare(a.version, b.version)); + return { + ...migrations, + [type.name]: { + latestVersion: _.last(transforms).version, + transforms, + }, + }; + }, {} as ActiveMigrations); } /** @@ -234,7 +239,7 @@ function buildDocumentTransform({ migrations: ActiveMigrations; validateDoc: ValidateDoc; }): TransformFn { - return function transformAndValidate(doc: RawSavedObjectDoc) { + return function transformAndValidate(doc: SavedObjectUnsanitizedDoc) { const result = doc.migrationVersion ? applyMigrations(doc, migrations) : markAsUpToDate(doc, migrations); @@ -252,7 +257,7 @@ function buildDocumentTransform({ }; } -function applyMigrations(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { +function applyMigrations(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) { while (true) { const prop = nextUnmigratedProp(doc, migrations); if (!prop) { @@ -265,14 +270,14 @@ function applyMigrations(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { /** * Gets the doc's props, handling the special case of "type". */ -function props(doc: RawSavedObjectDoc) { +function props(doc: SavedObjectUnsanitizedDoc) { return Object.keys(doc).concat(doc.type); } /** * Looks up the prop version in a saved object document or in our latest migrations. */ -function propVersion(doc: RawSavedObjectDoc | ActiveMigrations, prop: string) { +function propVersion(doc: SavedObjectUnsanitizedDoc | ActiveMigrations, prop: string) { return ( (doc[prop] && doc[prop].latestVersion) || (doc.migrationVersion && (doc as any).migrationVersion[prop]) @@ -282,7 +287,7 @@ function propVersion(doc: RawSavedObjectDoc | ActiveMigrations, prop: string) { /** * Sets the doc's migrationVersion to be the most recent version */ -function markAsUpToDate(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { +function markAsUpToDate(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) { return { ...doc, migrationVersion: props(doc).reduce((acc, prop) => { @@ -296,20 +301,25 @@ 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: MigrationFn, log: Logger) { - return function tryTransformDoc(doc: RawSavedObjectDoc) { +function wrapWithTry( + version: string, + type: string, + migrationFn: SavedObjectMigrationFn, + log: Logger +) { + return function tryTransformDoc(doc: SavedObjectUnsanitizedDoc) { try { - const result = transform(doc, new MigrationLogger(log)); + const result = migrationFn(doc, new MigrationLogger(log)); // A basic sanity check to help migration authors detect basic errors // (e.g. forgetting to return the transformed doc) if (!result || !result.type) { - throw new Error(`Invalid saved object returned from migration ${prop}:${version}.`); + throw new Error(`Invalid saved object returned from migration ${type}:${version}.`); } return result; } catch (error) { - const failedTransform = `${prop}:${version}`; + const failedTransform = `${type}:${version}`; const failedDoc = JSON.stringify(doc); log.warn( `Failed to transform document ${doc}. Transform: ${failedTransform}\nDoc: ${failedDoc}` @@ -322,7 +332,7 @@ function wrapWithTry(version: string, prop: string, transform: MigrationFn, log: /** * Finds the first unmigrated property in the specified document. */ -function nextUnmigratedProp(doc: RawSavedObjectDoc, migrations: ActiveMigrations) { +function nextUnmigratedProp(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrations) { return props(doc).find(p => { const latestVersion = propVersion(migrations, p); const docVersion = propVersion(doc, p); @@ -352,10 +362,10 @@ function nextUnmigratedProp(doc: RawSavedObjectDoc, migrations: ActiveMigrations * Applies any relevent migrations to the document for the specified property. */ function migrateProp( - doc: RawSavedObjectDoc, + doc: SavedObjectUnsanitizedDoc, prop: string, migrations: ActiveMigrations -): RawSavedObjectDoc { +): SavedObjectUnsanitizedDoc { const originalType = doc.type; let migrationVersion = _.clone(doc.migrationVersion) || {}; const typeChanged = () => !doc.hasOwnProperty(prop) || doc.type !== originalType; @@ -376,7 +386,11 @@ function migrateProp( /** * Retrieves any prop transforms that have not been applied to doc. */ -function applicableTransforms(migrations: ActiveMigrations, doc: RawSavedObjectDoc, prop: string) { +function applicableTransforms( + migrations: ActiveMigrations, + doc: SavedObjectUnsanitizedDoc, + prop: string +) { const minVersion = propVersion(doc, prop); const { transforms } = migrations[prop]; return minVersion @@ -389,7 +403,7 @@ function applicableTransforms(migrations: ActiveMigrations, doc: RawSavedObjectD * has not mutated migrationVersion in an unsupported way. */ function updateMigrationVersion( - doc: RawSavedObjectDoc, + doc: SavedObjectUnsanitizedDoc, migrationVersion: SavedObjectsMigrationVersion, prop: string, version: string @@ -405,7 +419,7 @@ function updateMigrationVersion( * as this could get us into an infinite loop. So, we explicitly check for that here. */ function assertNoDowngrades( - doc: RawSavedObjectDoc, + doc: SavedObjectUnsanitizedDoc, migrationVersion: SavedObjectsMigrationVersion, prop: string, version: string 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 2fc65f6e475d8..a9d0a339c229f 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 @@ -18,8 +18,8 @@ */ import _ from 'lodash'; -import { SavedObjectsSchema } from '../../schema'; -import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; +import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { IndexMigrator } from './index_migrator'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; @@ -39,7 +39,7 @@ describe('IndexMigrator', () => { migrationVersion: {}, migrate: _.identity, }, - serializer: new SavedObjectsSerializer(new SavedObjectsSchema()), + serializer: new SavedObjectsSerializer(new SavedObjectTypeRegistry()), }; }); @@ -254,7 +254,7 @@ describe('IndexMigrator', () => { test('transforms all docs from the original index', async () => { let count = 0; const { callCluster } = testOpts; - const migrateDoc = jest.fn((doc: RawSavedObjectDoc) => { + const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { return { ...doc, attributes: { name: ++count }, diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index aa208a23954f0..89f3fde384848 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -18,17 +18,21 @@ */ import _ from 'lodash'; -import { SavedObjectsSchema } from '../../schema'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsSerializer } from '../../serialization'; import { migrateRawDocs } from './migrate_raw_docs'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { const transform = jest.fn((doc: any) => _.set(doc, 'attributes.name', 'HOI!')); - const result = migrateRawDocs(new SavedObjectsSerializer(new SavedObjectsSchema()), transform, [ - { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, - { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ]); + const result = migrateRawDocs( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [ + { _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ] + ); expect(result).toEqual([ { @@ -48,10 +52,14 @@ describe('migrateRawDocs', () => { const transform = jest.fn((doc: any) => _.set(_.cloneDeep(doc), 'attributes.name', 'TADA') ); - const result = migrateRawDocs(new SavedObjectsSerializer(new SavedObjectsSchema()), transform, [ - { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, - { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, - ]); + const result = migrateRawDocs( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [ + { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, + { _id: 'c:d', _source: { type: 'c', c: { name: 'DDD' } } }, + ] + ); expect(result).toEqual([ { _id: 'foo:b', _source: { type: 'a', a: { name: 'AAA' } } }, diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index c008b18619629..5fe15f40db8ec 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -21,7 +21,7 @@ * This file provides logic for migrating raw documents. */ -import { RawDoc, SavedObjectsSerializer } from '../../serialization'; +import { SavedObjectsRawDoc, SavedObjectsSerializer } from '../../serialization'; import { TransformFn } from './document_migrator'; /** @@ -29,14 +29,14 @@ import { TransformFn } from './document_migrator'; * of raw docs. Any raw docs that are not valid saved objects will simply be passed through. * * @param {TransformFn} migrateDoc - * @param {RawDoc[]} rawDocs - * @returns {RawDoc[]} + * @param {SavedObjectsRawDoc[]} rawDocs + * @returns {SavedObjectsRawDoc[]} */ export function migrateRawDocs( serializer: SavedObjectsSerializer, migrateDoc: TransformFn, - rawDocs: RawDoc[] -): RawDoc[] { + rawDocs: SavedObjectsRawDoc[] +): SavedObjectsRawDoc[] { return rawDocs.map(raw => { if (serializer.isRawSavedObject(raw)) { const savedObject = serializer.rawToSavedObject(raw); 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 d4e97ee6c5747..3a6145f5d9623 100644 --- a/src/core/server/saved_objects/migrations/core/migration_context.ts +++ b/src/core/server/saved_objects/migrations/core/migration_context.ts @@ -26,7 +26,7 @@ import { Logger } from 'src/core/server/logging'; import { SavedObjectsSerializer } from '../../serialization'; -import { MappingProperties } from '../../mappings'; +import { SavedObjectsTypeMappingDefinitions } from '../../mappings'; import { buildActiveMappings } from './build_active_mappings'; import { CallCluster } from './call_cluster'; import { VersionedTransformer } from './document_migrator'; @@ -40,7 +40,7 @@ export interface MigrationOpts { callCluster: CallCluster; index: string; log: Logger; - mappingProperties: MappingProperties; + mappingProperties: SavedObjectsTypeMappingDefinitions; documentMigrator: VersionedTransformer; serializer: SavedObjectsSerializer; convertToAliasScript?: string; @@ -107,9 +107,9 @@ function createSourceContext(source: FullIndexInfo, alias: string) { function createDestContext( source: FullIndexInfo, alias: string, - mappingProperties: MappingProperties + mappingProperties: SavedObjectsTypeMappingDefinitions ): FullIndexInfo { - const activeMappings = buildActiveMappings({ properties: mappingProperties }); + const activeMappings = buildActiveMappings(mappingProperties); return { aliases: {}, @@ -133,6 +133,5 @@ function createDestContext( function nextIndexName(indexName: string, alias: string) { const indexSuffix = (indexName.match(/[\d]+$/) || [])[0]; const indexNum = parseInt(indexSuffix, 10) || 0; - return `${alias}_${indexNum + 1}`; } diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts index 818ca73b37189..800edaeaa5885 100644 --- a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts +++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts @@ -24,6 +24,7 @@ describe('coordinateMigration', () => { const log = { debug: jest.fn(), warning: jest.fn(), + warn: jest.fn(), info: jest.fn(), }; 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 7c61d0c48d9bd..9dfb3abc8e72d 100644 --- a/src/core/server/saved_objects/migrations/core/migration_logger.ts +++ b/src/core/server/saved_objects/migrations/core/migration_logger.ts @@ -30,7 +30,11 @@ export type LogFn = (path: string[], message: string) => void; export interface SavedObjectsMigrationLogger { debug: (msg: string) => void; info: (msg: string) => void; + /** + * @deprecated Use `warn` instead. + */ warning: (msg: string) => void; + warn: (msg: string) => void; } export class MigrationLogger implements SavedObjectsMigrationLogger { @@ -43,4 +47,5 @@ export class MigrationLogger implements SavedObjectsMigrationLogger { public info = (msg: string) => this.logger.info(msg); public debug = (msg: string) => this.logger.debug(msg); public warning = (msg: string) => this.logger.warn(msg); + public warn = (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 6cb7ecac92ab9..e96986bd702e6 100644 --- a/src/core/server/saved_objects/migrations/index.ts +++ b/src/core/server/saved_objects/migrations/index.ts @@ -18,3 +18,4 @@ */ export { KibanaMigrator, IKibanaMigrator } from './kibana'; +export { SavedObjectMigrationFn, SavedObjectMigrationMap } from './types'; diff --git a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap index 792b2063fc86d..37a73b11bbc48 100644 --- a/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap +++ b/src/core/server/saved_objects/migrations/kibana/__snapshots__/kibana_migrator.test.ts.snap @@ -4,8 +4,8 @@ exports[`KibanaMigrator getActiveMappings returns full index mappings w/ core pr Object { "_meta": Object { "migrationMappingPropertyHashes": Object { - "amap": "625b32086eb1d1203564cf85062dd22e", - "bmap": "625b32086eb1d1203564cf85062dd22e", + "amap": "510f1f0adb69830cf8a1c5ce2923ed82", + "bmap": "510f1f0adb69830cf8a1c5ce2923ed82", "config": "87aca8fdb053154f11383fce3dbf3edf", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "namespace": "2f4316de49999235636386fe51dc06c1", @@ -17,10 +17,18 @@ Object { "dynamic": "strict", "properties": Object { "amap": Object { - "type": "text", + "properties": Object { + "field": Object { + "type": "text", + }, + }, }, "bmap": Object { - "type": "text", + "properties": Object { + "field": Object { + "type": "text", + }, + }, }, "config": Object { "dynamic": "true", 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 index c1021018538d5..2ee656721abd0 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -19,28 +19,29 @@ import { KibanaMigrator } from './kibana_migrator'; import { buildActiveMappings } from '../core'; -import { SavedObjectsMapping } from '../../mappings'; -const { mergeProperties } = jest.requireActual('./kibana_migrator'); +const { mergeTypes } = jest.requireActual('./kibana_migrator'); +import { SavedObjectsType } from '../../types'; -const defaultSavedObjectMappings = [ +const defaultSavedObjectTypes: SavedObjectsType[] = [ { - pluginId: 'testplugin', - properties: { - testtype: { - properties: { - name: { type: 'keyword' }, - }, + name: 'testtype', + hidden: false, + namespaceAgnostic: false, + mappings: { + properties: { + name: { type: 'keyword' }, }, }, + migrations: {}, }, ]; const createMigrator = ( { - savedObjectMappings, + types, }: { - savedObjectMappings: SavedObjectsMapping[]; - } = { savedObjectMappings: defaultSavedObjectMappings } + types: SavedObjectsType[]; + } = { types: defaultSavedObjectTypes } ) => { const mockMigrator: jest.Mocked> = { runMigrations: jest.fn(), @@ -48,9 +49,7 @@ const createMigrator = ( migrateDocument: jest.fn(), }; - mockMigrator.getActiveMappings.mockReturnValue( - buildActiveMappings({ properties: mergeProperties(savedObjectMappings) }) - ); + mockMigrator.getActiveMappings.mockReturnValue(buildActiveMappings(mergeTypes(types))); mockMigrator.migrateDocument.mockImplementation(doc => doc); return mockMigrator; }; 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 c6a72eb53d6c4..fd82bf282266e 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 @@ -17,45 +17,49 @@ * under the License. */ -import _ from 'lodash'; import { KibanaMigratorOptions, KibanaMigrator } from './kibana_migrator'; import { loggingServiceMock } from '../../../logging/logging_service.mock'; -import { SavedObjectsSchema } from '../../schema'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { SavedObjectsType } from '../../types'; + +const createRegistry = (types: Array>) => { + const registry = new SavedObjectTypeRegistry(); + types.forEach(type => + registry.registerType({ + name: 'unknown', + namespaceAgnostic: false, + hidden: false, + mappings: { properties: {} }, + migrations: {}, + ...type, + }) + ); + return registry; +}; describe('KibanaMigrator', () => { describe('getActiveMappings', () => { it('returns full index mappings w/ core properties', () => { const options = mockOptions(); - options.savedObjectMappings = [ + options.typeRegistry = createRegistry([ { - pluginId: 'aaa', - properties: { amap: { type: 'text' } }, + name: 'amap', + mappings: { + properties: { field: { type: 'text' } }, + }, }, { - pluginId: 'bbb', - properties: { bmap: { type: 'text' } }, + name: 'bmap', + indexPattern: 'other-index', + mappings: { + properties: { field: { type: 'text' } }, + }, }, - ]; + ]); + const mappings = new KibanaMigrator(options).getActiveMappings(); expect(mappings).toMatchSnapshot(); }); - - it('Fails if duplicate mappings are defined', () => { - const options = mockOptions(); - options.savedObjectMappings = [ - { - pluginId: 'aaa', - properties: { amap: { type: 'text' } }, - }, - { - pluginId: 'bbb', - properties: { amap: { type: 'long' } }, - }, - ]; - expect(() => new KibanaMigrator(options).getActiveMappings()).toThrow( - /Plugin bbb is attempting to redefine mapping "amap"/ - ); - }); }); describe('runMigrations', () => { @@ -78,41 +82,37 @@ describe('KibanaMigrator', () => { }); }); -function mockOptions({ configValues }: { configValues?: any } = {}): KibanaMigratorOptions { +function mockOptions(): KibanaMigratorOptions { const callCluster = jest.fn(); return { logger: loggingServiceMock.create().get(), kibanaVersion: '8.2.3', savedObjectValidations: {}, - savedObjectMigrations: {}, - savedObjectMappings: [ + typeRegistry: createRegistry([ { - pluginId: 'testtype', - properties: { - testtype: { - properties: { - name: { type: 'keyword' }, - }, + name: 'testtype', + hidden: false, + namespaceAgnostic: false, + mappings: { + properties: { + name: { type: 'keyword' }, }, }, + migrations: {}, }, { - pluginId: 'testtype2', - properties: { - testtype2: { - properties: { - name: { type: 'keyword' }, - }, + name: 'testtype2', + hidden: false, + namespaceAgnostic: false, + indexPattern: 'other-index', + mappings: { + properties: { + name: { type: 'keyword' }, }, }, + migrations: {}, }, - ], - savedObjectSchemas: new SavedObjectsSchema({ - testtype2: { - isNamespaceAgnostic: false, - indexPattern: 'other-index', - }, - }), + ]), kibanaConfig: { enabled: true, index: '.my-index', @@ -123,15 +123,6 @@ function mockOptions({ configValues }: { configValues?: any } = {}): KibanaMigra 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, }; } 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 747b48a540109..9b4fe10a35100 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -24,30 +24,23 @@ import { Logger } from 'src/core/server/logging'; import { KibanaConfigType } from 'src/core/server/kibana_config'; -import { MappingProperties, SavedObjectsMapping, IndexMapping } from '../../mappings'; -import { SavedObjectsSchema } from '../../schema'; -import { RawSavedObjectDoc, SavedObjectsSerializer } from '../../serialization'; +import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; +import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; import { docValidator, PropertyValidators } from '../../validation'; import { buildActiveMappings, CallCluster, IndexMigrator } from '../core'; -import { - DocumentMigrator, - VersionedTransformer, - MigrationDefinition, -} from '../core/document_migrator'; +import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; import { createIndexMap } from '../core/build_index_map'; import { SavedObjectsConfigType } from '../../saved_objects_config'; -import { LegacyConfig } from '../../../legacy'; +import { ISavedObjectTypeRegistry } from '../../saved_objects_type_registry'; +import { SavedObjectsType } from '../../types'; export interface KibanaMigratorOptions { callCluster: CallCluster; - config: LegacyConfig; + typeRegistry: ISavedObjectTypeRegistry; savedObjectsConfig: SavedObjectsConfigType; kibanaConfig: KibanaConfigType; kibanaVersion: string; logger: Logger; - savedObjectMappings: SavedObjectsMapping[]; - savedObjectMigrations: MigrationDefinition; - savedObjectSchemas: SavedObjectsSchema; savedObjectValidations: PropertyValidators; } @@ -58,13 +51,12 @@ export type IKibanaMigrator = Pick; */ export class KibanaMigrator { private readonly callCluster: CallCluster; - private readonly config: LegacyConfig; 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 mappingProperties: SavedObjectsTypeMappingDefinitions; + private readonly typeRegistry: ISavedObjectTypeRegistry; private readonly serializer: SavedObjectsSerializer; private migrationResult?: Promise>; @@ -73,27 +65,23 @@ export class KibanaMigrator { */ constructor({ callCluster, - config, + typeRegistry, kibanaConfig, savedObjectsConfig, + savedObjectValidations, kibanaVersion, logger, - savedObjectMappings, - savedObjectMigrations, - savedObjectSchemas, - savedObjectValidations, }: KibanaMigratorOptions) { - this.config = config; this.callCluster = callCluster; this.kibanaConfig = kibanaConfig; this.savedObjectsConfig = savedObjectsConfig; - this.schema = savedObjectSchemas; - this.serializer = new SavedObjectsSerializer(this.schema); - this.mappingProperties = mergeProperties(savedObjectMappings || []); + this.typeRegistry = typeRegistry; + this.serializer = new SavedObjectsSerializer(this.typeRegistry); + this.mappingProperties = mergeTypes(this.typeRegistry.getAllTypes()); this.log = logger; this.documentMigrator = new DocumentMigrator({ kibanaVersion, - migrations: savedObjectMigrations || {}, + typeRegistry, validateDoc: docValidator(savedObjectValidations || {}), log: this.log, }); @@ -118,10 +106,9 @@ export class KibanaMigrator { private runMigrationsInternal() { const kibanaIndexName = this.kibanaConfig.index; const indexMap = createIndexMap({ - config: this.config, kibanaIndexName, indexMap: this.mappingProperties, - schema: this.schema, + registry: this.typeRegistry, }); const migrators = Object.keys(indexMap).map(index => { @@ -150,7 +137,7 @@ export class KibanaMigrator { * */ public getActiveMappings(): IndexMapping { - return buildActiveMappings({ properties: this.mappingProperties }); + return buildActiveMappings(this.mappingProperties); } /** @@ -159,7 +146,7 @@ export class KibanaMigrator { * @param doc - The saved object to migrate * @returns `doc` with all registered migrations applied. */ - public migrateDocument(doc: RawSavedObjectDoc): RawSavedObjectDoc { + public migrateDocument(doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc { return this.documentMigrator.migrate(doc); } } @@ -168,12 +155,15 @@ export class KibanaMigrator { * Merges savedObjectMappings properties into a single object, verifying that * no mappings are redefined. */ -export function mergeProperties(mappings: SavedObjectsMapping[]): MappingProperties { - return mappings.reduce((acc, { pluginId, properties }) => { - const duplicate = Object.keys(properties).find(k => acc.hasOwnProperty(k)); +export function mergeTypes(types: SavedObjectsType[]): SavedObjectsTypeMappingDefinitions { + return types.reduce((acc, { name: type, mappings }) => { + const duplicate = acc.hasOwnProperty(type); if (duplicate) { - throw new Error(`Plugin ${pluginId} is attempting to redefine mapping "${duplicate}".`); + throw new Error(`Type ${type} is already defined.`); } - return Object.assign(acc, properties); + return { + ...acc, + [type]: mappings, + }; }, {}); } diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts new file mode 100644 index 0000000000000..01741dd2ded1a --- /dev/null +++ b/src/core/server/saved_objects/migrations/types.ts @@ -0,0 +1,51 @@ +/* + * 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 { SavedObjectUnsanitizedDoc } from '../serialization'; +import { SavedObjectsMigrationLogger } from './core/migration_logger'; + +/** + * A migration function defined for a {@link SavedObjectsType | saved objects type} + * used to migrate it's {@link SavedObjectUnsanitizedDoc | documents} + */ +export type SavedObjectMigrationFn = ( + doc: SavedObjectUnsanitizedDoc, + log: SavedObjectsMigrationLogger +) => SavedObjectUnsanitizedDoc; + +/** + * A map of {@link SavedObjectMigrationFn | migration functions} to be used for a given type. + * The map's keys must be valid semver versions. + * + * For a given document, only migrations with a higher version number than that of the document will be applied. + * Migrations are executed in order, starting from the lowest version and ending with the highest one. + * + * @example + * ```typescript + * const migrations: SavedObjectMigrationMap = { + * '1.0.0': migrateToV1, + * '2.1.0': migrateToV21 + * } + * ``` + * + * @public + */ +export interface SavedObjectMigrationMap { + [version: string]: SavedObjectMigrationFn; +} diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index a15d1f5b864b7..70f3d5a5b18e4 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -21,38 +21,60 @@ import { SavedObjectsService, InternalSavedObjectsServiceSetup, InternalSavedObjectsServiceStart, + SavedObjectsServiceSetup, + SavedObjectsServiceStart, } from './saved_objects_service'; import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock'; import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; +import { typeRegistryMock } from './saved_objects_type_registry.mock'; type SavedObjectsServiceContract = PublicMethodsOf; const createStartContractMock = () => { - const startContract: jest.Mocked = { - clientProvider: savedObjectsClientProviderMock.create(), + const startContrat: jest.Mocked = { getScopedClient: jest.fn(), createInternalRepository: jest.fn(), createScopedRepository: jest.fn(), - migrator: mockKibanaMigrator.create(), + createSerializer: jest.fn(), }; - startContract.getScopedClient.mockReturnValue(savedObjectsClientMock.create()); - startContract.createInternalRepository.mockReturnValue(savedObjectsRepositoryMock.create()); - startContract.createScopedRepository.mockReturnValue(savedObjectsRepositoryMock.create()); + startContrat.getScopedClient.mockReturnValue(savedObjectsClientMock.create()); + startContrat.createInternalRepository.mockReturnValue(savedObjectsRepositoryMock.create()); + startContrat.createScopedRepository.mockReturnValue(savedObjectsRepositoryMock.create()); + + return startContrat; +}; + +const createInternalStartContractMock = () => { + const internalStartContract: jest.Mocked = { + ...createStartContractMock(), + clientProvider: savedObjectsClientProviderMock.create(), + migrator: mockKibanaMigrator.create(), + typeRegistry: typeRegistryMock.create(), + }; - return startContract; + return internalStartContract; }; const createSetupContractMock = () => { - const setupContract: jest.Mocked = { + const setupContract: jest.Mocked = { setClientFactoryProvider: jest.fn(), addClientWrapper: jest.fn(), }; + return setupContract; }; +const createInternalSetupContractMock = () => { + const internalSetupContract: jest.Mocked = { + ...createSetupContractMock(), + registerType: jest.fn(), + }; + return internalSetupContract; +}; + const createSavedObjectsServiceMock = () => { const mocked: jest.Mocked = { setup: jest.fn(), @@ -60,14 +82,16 @@ const createSavedObjectsServiceMock = () => { stop: jest.fn(), }; - mocked.setup.mockResolvedValue(createSetupContractMock()); - mocked.start.mockResolvedValue(createStartContractMock()); + mocked.setup.mockResolvedValue(createInternalSetupContractMock()); + mocked.start.mockResolvedValue(createInternalStartContractMock()); mocked.stop.mockResolvedValue(); return mocked; }; export const savedObjectsServiceMock = { create: createSavedObjectsServiceMock, + createInternalSetupContract: createInternalSetupContractMock, createSetupContract: createSetupContractMock, + createInternalStartContract: createInternalStartContractMock, createStartContract: createStartContractMock, }; diff --git a/src/core/server/saved_objects/saved_objects_service.test.mocks.ts b/src/core/server/saved_objects/saved_objects_service.test.mocks.ts index a9ad0778d4757..8c1cd3411203a 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.mocks.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.mocks.ts @@ -19,6 +19,7 @@ import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock'; +import { typeRegistryMock } from './saved_objects_type_registry.mock'; export const migratorInstanceMock = mockKibanaMigrator.create(); export const KibanaMigratorMock = jest.fn().mockImplementation(() => migratorInstanceMock); @@ -30,3 +31,8 @@ export const clientProviderInstanceMock = savedObjectsClientProviderMock.create( jest.doMock('./service/lib/scoped_client_provider', () => ({ SavedObjectsClientProvider: jest.fn().mockImplementation(() => clientProviderInstanceMock), })); + +export const typeRegistryInstanceMock = typeRegistryMock.create(); +jest.doMock('./saved_objects_type_registry', () => ({ + SavedObjectTypeRegistry: jest.fn().mockImplementation(() => typeRegistryInstanceMock), +})); diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 19798aa68928d..07e7d8db8b25c 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -21,6 +21,7 @@ import { KibanaMigratorMock, migratorInstanceMock, clientProviderInstanceMock, + typeRegistryInstanceMock, } from './saved_objects_service.test.mocks'; import { SavedObjectsService } from './saved_objects_service'; @@ -108,6 +109,25 @@ describe('SavedObjectsService', () => { ); }); }); + + describe('registerType', () => { + it('registers the type to the internal typeRegistry', async () => { + const coreContext = mockCoreContext.create(); + const soService = new SavedObjectsService(coreContext); + const setup = await soService.setup(createSetupDeps()); + + const type = { + name: 'someType', + hidden: false, + namespaceAgnostic: false, + mappings: { properties: {} }, + }; + setup.registerType(type); + + expect(typeRegistryInstanceMock.registerType).toHaveBeenCalledTimes(1); + expect(typeRegistryInstanceMock.registerType).toHaveBeenCalledWith(type); + }); + }); }); describe('#start()', () => { diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 0c985c71c7e2f..2f07dbe51a09e 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -21,7 +21,6 @@ import { CoreService } from 'src/core/types'; import { first, filter, take } from 'rxjs/operators'; import { SavedObjectsClient, - SavedObjectsSchema, SavedObjectsClientProvider, ISavedObjectsClientProvider, SavedObjectsClientProviderOptions, @@ -34,17 +33,17 @@ import { KibanaConfigType } from '../kibana_config'; import { migrationsRetryCallCluster } from '../elasticsearch/retry_call_cluster'; import { SavedObjectsConfigType } from './saved_objects_config'; import { KibanaRequest } from '../http'; -import { SavedObjectsClientContract } from './types'; +import { SavedObjectsClientContract, SavedObjectsType } from './types'; import { ISavedObjectsRepository, SavedObjectsRepository } from './service/lib/repository'; import { SavedObjectsClientFactoryProvider, SavedObjectsClientWrapperFactory, } from './service/lib/scoped_client_provider'; import { Logger } from '../logging'; -import { SavedObjectsMapping } from './mappings'; -import { MigrationDefinition } from './migrations/core/document_migrator'; -import { SavedObjectsSchemaDefinition } from './schema'; +import { convertLegacyTypes } from './utils'; +import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; import { PropertyValidators } from './validation'; +import { SavedObjectsSerializer } from './serialization'; /** * Saved Objects is Kibana's data persistence mechanism allowing plugins to @@ -70,6 +69,7 @@ import { PropertyValidators } from './validation'; * constructor. * * @example + * ```ts * import { SavedObjectsClient, CoreSetup } from 'src/core/server'; * * export class Plugin() { @@ -79,6 +79,7 @@ import { PropertyValidators } from './validation'; * }) * } * } + * ``` * * @public */ @@ -102,7 +103,9 @@ export interface SavedObjectsServiceSetup { /** * @internal */ -export type InternalSavedObjectsServiceSetup = SavedObjectsServiceSetup; +export interface InternalSavedObjectsServiceSetup extends SavedObjectsServiceSetup { + registerType: (type: SavedObjectsType) => void; +} /** * Saved Objects is Kibana's data persisentence mechanism allowing plugins to @@ -146,6 +149,10 @@ export interface SavedObjectsServiceStart { * @param extraTypes - A list of additional hidden types the repository should have access to. */ createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository; + /** + * Creates a {@link SavedObjectsSerializer | serializer} that is aware of all registered types. + */ + createSerializer: () => SavedObjectsSerializer; } export interface InternalSavedObjectsServiceStart extends SavedObjectsServiceStart { @@ -157,6 +164,10 @@ export interface InternalSavedObjectsServiceStart extends SavedObjectsServiceSta * @deprecated Exposed only for injecting into Legacy */ clientProvider: ISavedObjectsClientProvider; + /** + * @deprecated Exposed only for injecting into Legacy + */ + typeRegistry: ISavedObjectTypeRegistry; } /** @@ -207,9 +218,7 @@ export class SavedObjectsService private clientFactoryProvider?: SavedObjectsClientFactoryProvider; private clientFactoryWrappers: WrappedClientFactoryWrapper[] = []; - private mappings: SavedObjectsMapping[] = []; - private migrations: MigrationDefinition = {}; - private schemas: SavedObjectsSchemaDefinition = {}; + private typeRegistry = new SavedObjectTypeRegistry(); private validations: PropertyValidators = {}; constructor(private readonly coreContext: CoreContext) { @@ -221,16 +230,12 @@ export class SavedObjectsService this.setupDeps = setupDeps; - const { - savedObjectSchemas: savedObjectsSchemasDefinition, - savedObjectMappings, - savedObjectMigrations, - savedObjectValidations, - } = setupDeps.legacyPlugins.uiExports; - this.mappings = savedObjectMappings; - this.migrations = savedObjectMigrations; - this.schemas = savedObjectsSchemasDefinition; - this.validations = savedObjectValidations; + const legacyTypes = convertLegacyTypes( + setupDeps.legacyPlugins.uiExports, + setupDeps.legacyPlugins.pluginExtendedConfig + ); + legacyTypes.forEach(type => this.typeRegistry.registerType(type)); + this.validations = setupDeps.legacyPlugins.uiExports.savedObjectValidations || {}; return { setClientFactoryProvider: provider => { @@ -246,6 +251,9 @@ export class SavedObjectsService factory, }); }, + registerType: type => { + this.typeRegistry.registerType(type); + }, }; } @@ -303,8 +311,7 @@ export class SavedObjectsService const createRepository = (callCluster: APICaller, extraTypes: string[] = []) => { return SavedObjectsRepository.createRepository( migrator, - new SavedObjectsSchema(this.schemas), - this.setupDeps!.legacyPlugins.pluginExtendedConfig, + this.typeRegistry, kibanaConfig.index, callCluster, extraTypes @@ -335,9 +342,11 @@ export class SavedObjectsService return { migrator, clientProvider, + typeRegistry: this.typeRegistry, getScopedClient: clientProvider.getClient.bind(clientProvider), createScopedRepository: repositoryFactory.createScopedRepository, createInternalRepository: repositoryFactory.createInternalRepository, + createSerializer: () => new SavedObjectsSerializer(this.typeRegistry), }; } @@ -348,18 +357,14 @@ export class SavedObjectsService savedObjectsConfig: SavedObjectsConfigType, migrationsRetryDelay?: number ): KibanaMigrator { - const savedObjectSchemas = new SavedObjectsSchema(this.schemas); const adminClient = this.setupDeps!.elasticsearch.adminClient; return new KibanaMigrator({ - savedObjectSchemas, - savedObjectMappings: this.mappings, - savedObjectMigrations: this.migrations, - savedObjectValidations: this.validations, + typeRegistry: this.typeRegistry, logger: this.logger, kibanaVersion: this.coreContext.env.packageInfo.version, - config: this.setupDeps!.legacyPlugins.pluginExtendedConfig, savedObjectsConfig, + savedObjectValidations: this.validations, kibanaConfig, callCluster: migrationsRetryCallCluster( adminClient.callAsInternalUser, diff --git a/src/core/server/saved_objects/schema/schema.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts similarity index 58% rename from src/core/server/saved_objects/schema/schema.mock.ts rename to src/core/server/saved_objects/saved_objects_type_registry.mock.ts index 89c318f087ab0..6e11920db6b7d 100644 --- a/src/core/server/saved_objects/schema/schema.mock.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts @@ -17,19 +17,25 @@ * under the License. */ -import { SavedObjectsSchema } from './schema'; +import { ISavedObjectTypeRegistry } from './saved_objects_type_registry'; -type Schema = PublicMethodsOf; -const createSchemaMock = () => { - const mocked: jest.Mocked = { - getIndexForType: jest.fn().mockReturnValue('.kibana-test'), - isHiddenType: jest.fn().mockReturnValue(false), - isNamespaceAgnostic: jest.fn((type: string) => type === 'global'), - getConvertToAliasScript: jest.fn().mockReturnValue(undefined), +const createRegistryMock = (): jest.Mocked => { + const mock = { + registerType: jest.fn(), + getType: jest.fn(), + getAllTypes: jest.fn(), + isNamespaceAgnostic: jest.fn(), + isHidden: jest.fn(), + getIndex: jest.fn(), }; - return mocked; + + mock.getIndex.mockReturnValue('.kibana-test'); + mock.isHidden.mockReturnValue(false); + mock.isNamespaceAgnostic.mockImplementation((type: string) => type === 'global'); + + return mock; }; -export const schemaMock = { - create: createSchemaMock, +export const typeRegistryMock = { + create: createRegistryMock, }; diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts new file mode 100644 index 0000000000000..4268ab7718f8d --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -0,0 +1,215 @@ +/* + * 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 { SavedObjectTypeRegistry } from './saved_objects_type_registry'; +import { SavedObjectsType } from './types'; + +const createType = (type: Partial): SavedObjectsType => ({ + name: 'unknown', + hidden: false, + namespaceAgnostic: false, + mappings: { properties: {} }, + migrations: {}, + ...type, +}); + +describe('SavedObjectTypeRegistry', () => { + let registry: SavedObjectTypeRegistry; + + beforeEach(() => { + registry = new SavedObjectTypeRegistry(); + }); + + it('allows to register types', () => { + registry.registerType(createType({ name: 'typeA' })); + registry.registerType(createType({ name: 'typeB' })); + registry.registerType(createType({ name: 'typeC' })); + + expect( + registry + .getAllTypes() + .map(type => type.name) + .sort() + ).toEqual(['typeA', 'typeB', 'typeC']); + }); + + it('throws when trying to register the same type twice', () => { + registry.registerType(createType({ name: 'typeA' })); + registry.registerType(createType({ name: 'typeB' })); + expect(() => { + registry.registerType(createType({ name: 'typeA' })); + }).toThrowErrorMatchingInlineSnapshot(`"Type 'typeA' is already registered"`); + }); + + describe('#getType', () => { + it(`retrieve a type by it's name`, () => { + const typeA = createType({ name: 'typeA' }); + const typeB = createType({ name: 'typeB' }); + registry.registerType(typeA); + registry.registerType(typeB); + registry.registerType(createType({ name: 'typeC' })); + + expect(registry.getType('typeA')).toEqual(typeA); + expect(registry.getType('typeB')).toEqual(typeB); + expect(registry.getType('unknownType')).toBeUndefined(); + }); + + it('forbids to mutate the registered types', () => { + registry.registerType( + createType({ + name: 'typeA', + mappings: { + properties: { + someField: { type: 'text' }, + }, + }, + }) + ); + + const typeA = registry.getType('typeA')!; + + expect(() => { + typeA.migrations = {}; + }).toThrow(); + expect(() => { + typeA.name = 'foo'; + }).toThrow(); + expect(() => { + typeA.mappings.properties = {}; + }).toThrow(); + expect(() => { + typeA.indexPattern = '.overrided'; + }).toThrow(); + }); + }); + + describe('#getAllTypes', () => { + it('returns all registered types', () => { + const typeA = createType({ name: 'typeA' }); + const typeB = createType({ name: 'typeB' }); + const typeC = createType({ name: 'typeC' }); + registry.registerType(typeA); + registry.registerType(typeB); + + const registered = registry.getAllTypes(); + expect(registered.length).toEqual(2); + expect(registered).toContainEqual(typeA); + expect(registered).toContainEqual(typeB); + expect(registered).not.toContainEqual(typeC); + }); + + it('forbids to mutate the registered types', () => { + registry.registerType( + createType({ + name: 'typeA', + mappings: { + properties: { + someField: { type: 'text' }, + }, + }, + }) + ); + registry.registerType( + createType({ + name: 'typeB', + migrations: { + '1.0.0': jest.fn(), + }, + }) + ); + + const typeA = registry.getType('typeA')!; + const typeB = registry.getType('typeB')!; + + expect(() => { + typeA.migrations = {}; + }).toThrow(); + expect(() => { + typeA.name = 'foo'; + }).toThrow(); + expect(() => { + typeB.mappings.properties = {}; + }).toThrow(); + expect(() => { + typeB.indexPattern = '.overrided'; + }).toThrow(); + }); + + it('does not mutate the registered types when altering the list', () => { + registry.registerType(createType({ name: 'typeA' })); + registry.registerType(createType({ name: 'typeB' })); + registry.registerType(createType({ name: 'typeC' })); + + const types = registry.getAllTypes(); + types.splice(0, 3); + + expect(registry.getAllTypes().length).toEqual(3); + }); + }); + + describe('#isNamespaceAgnostic', () => { + it('returns correct value for the type', () => { + registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); + registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); + + expect(registry.isNamespaceAgnostic('typeA')).toEqual(true); + expect(registry.isNamespaceAgnostic('typeB')).toEqual(false); + }); + it('returns false when the type is not registered', () => { + registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); + registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); + + expect(registry.isNamespaceAgnostic('unknownType')).toEqual(false); + }); + }); + + describe('#isHidden', () => { + it('returns correct value for the type', () => { + registry.registerType(createType({ name: 'typeA', hidden: true })); + registry.registerType(createType({ name: 'typeB', hidden: false })); + + expect(registry.isHidden('typeA')).toEqual(true); + expect(registry.isHidden('typeB')).toEqual(false); + }); + it('returns false when the type is not registered', () => { + registry.registerType(createType({ name: 'typeA', hidden: true })); + registry.registerType(createType({ name: 'typeB', hidden: false })); + + expect(registry.isHidden('unknownType')).toEqual(false); + }); + }); + + describe('#getIndex', () => { + it('returns correct value for the type', () => { + registry.registerType(createType({ name: 'typeA', indexPattern: '.custom-index' })); + registry.registerType(createType({ name: 'typeB', indexPattern: '.another-index' })); + registry.registerType(createType({ name: 'typeWithNoIndex' })); + + expect(registry.getIndex('typeA')).toEqual('.custom-index'); + expect(registry.getIndex('typeB')).toEqual('.another-index'); + expect(registry.getIndex('typeWithNoIndex')).toBeUndefined(); + }); + it('returns undefined when the type is not registered', () => { + registry.registerType(createType({ name: 'typeA', namespaceAgnostic: true })); + registry.registerType(createType({ name: 'typeB', namespaceAgnostic: false })); + + expect(registry.getIndex('unknownType')).toBeUndefined(); + }); + }); +}); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts new file mode 100644 index 0000000000000..3f26d696831fd --- /dev/null +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -0,0 +1,86 @@ +/* + * 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 { deepFreeze } from '../../utils'; +import { SavedObjectsType } from './types'; + +/** + * See {@link SavedObjectTypeRegistry} for documentation. + * + * @internal + * */ +export type ISavedObjectTypeRegistry = PublicMethodsOf; + +/** + * Registry holding information about all the registered {@link SavedObjectsType | savedObject types}. + * + * @internal + */ +export class SavedObjectTypeRegistry { + private readonly types = new Map(); + + /** + * Register a {@link SavedObjectsType | type} inside the registry. + * A type can only be registered once. subsequent calls with the same type name will throw an error. + */ + public registerType(type: SavedObjectsType) { + if (this.types.has(type.name)) { + throw new Error(`Type '${type.name}' is already registered`); + } + this.types.set(type.name, deepFreeze(type)); + } + + /** + * Return the {@link SavedObjectsType | type} definition for given type name. + */ + public getType(type: string) { + return this.types.get(type); + } + + /** + * Return all {@link SavedObjectsType | types} currently registered. + */ + public getAllTypes() { + return [...this.types.values()]; + } + + /** + * Returns the `namespaceAgnostic` property for given type, or `false` if + * the type is not registered. + */ + public isNamespaceAgnostic(type: string) { + return this.types.get(type)?.namespaceAgnostic ?? false; + } + + /** + * Returns the `hidden` property for given type, or `false` if + * the type is not registered. + */ + public isHidden(type: string) { + return this.types.get(type)?.hidden ?? false; + } + + /** + * Returns the `indexPattern` property for given type, or `undefined` if + * the type is not registered. + */ + public getIndex(type: string) { + return this.types.get(type)?.indexPattern; + } +} diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts index 00c4939233318..17ca406ea109a 100644 --- a/src/core/server/saved_objects/schema/schema.ts +++ b/src/core/server/saved_objects/schema/schema.ts @@ -19,6 +19,10 @@ import { LegacyConfig } from '../../legacy'; +/** + * @deprecated + * @internal + **/ interface SavedObjectsSchemaTypeDefinition { isNamespaceAgnostic: boolean; hidden?: boolean; @@ -26,12 +30,18 @@ interface SavedObjectsSchemaTypeDefinition { convertToAliasScript?: string; } -/** @internal */ +/** + * @deprecated + * @internal + **/ export interface SavedObjectsSchemaDefinition { - [key: string]: SavedObjectsSchemaTypeDefinition; + [type: string]: SavedObjectsSchemaTypeDefinition; } -/** @internal */ +/** + * @deprecated This is only used by the {@link SavedObjectsLegacyService | legacy savedObjects service} + * @internal + **/ export class SavedObjectsSchema { private readonly definition?: SavedObjectsSchemaDefinition; constructor(schemaDefinition?: SavedObjectsSchemaDefinition) { @@ -46,7 +56,6 @@ export class SavedObjectsSchema { return false; } - // TODO: Remove dependency on config when we move SavedObjectsSchema to NP public getIndexForType(config: LegacyConfig, type: string): string | undefined { if (this.definition != null && this.definition.hasOwnProperty(type)) { const { indexPattern } = this.definition[type]; diff --git a/src/core/server/saved_objects/serialization/index.ts b/src/core/server/saved_objects/serialization/index.ts index 217ffe7129e94..f7f4e75704341 100644 --- a/src/core/server/saved_objects/serialization/index.ts +++ b/src/core/server/saved_objects/serialization/index.ts @@ -22,167 +22,5 @@ * the raw document format as stored in ElasticSearch. */ -/* eslint-disable @typescript-eslint/camelcase */ - -import uuid from 'uuid'; -import { SavedObjectsSchema } from '../schema'; -import { decodeVersion, encodeVersion } from '../version'; -import { SavedObjectsMigrationVersion, SavedObjectReference } from '../types'; - -/** - * A raw document as represented directly in the saved object index. - */ -export interface RawDoc { - _id: string; - _source: any; - _type?: string; - _seq_no?: number; - _primary_term?: number; -} - -/** - * A saved object type definition that allows for miscellaneous, unknown - * properties, as current discussions around security, ACLs, etc indicate - * that future props are likely to be added. Migrations support this - * scenario out of the box. - */ -interface SavedObjectDoc { - attributes: object; - id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional - type: string; - namespace?: string; - migrationVersion?: SavedObjectsMigrationVersion; - version?: string; - updated_at?: string; - - [rootProp: string]: any; -} - -interface Referencable { - references: SavedObjectReference[]; -} - -/** - * We want to have two types, one that guarantees a "references" attribute - * will exist and one that allows it to be null. Since we're not migrating - * all the saved objects to have a "references" array, we need to support - * the scenarios where it may be missing (ex migrations). - */ -export type RawSavedObjectDoc = SavedObjectDoc & Partial; -export type SanitizedSavedObjectDoc = SavedObjectDoc & Referencable; - -function assertNonEmptyString(value: string, name: string) { - if (!value || typeof value !== 'string') { - throw new TypeError(`Expected "${value}" to be a ${name}`); - } -} - -/** @internal */ -export class SavedObjectsSerializer { - private readonly schema: SavedObjectsSchema; - - constructor(schema: SavedObjectsSchema) { - this.schema = schema; - } - /** - * Determines whether or not the raw document can be converted to a saved object. - * - * @param {RawDoc} rawDoc - The raw ES document to be tested - */ - public isRawSavedObject(rawDoc: RawDoc) { - const { type, namespace } = rawDoc._source; - const namespacePrefix = - namespace && !this.schema.isNamespaceAgnostic(type) ? `${namespace}:` : ''; - return ( - type && - rawDoc._id.startsWith(`${namespacePrefix}${type}:`) && - rawDoc._source.hasOwnProperty(type) - ); - } - - /** - * Converts a document from the format that is stored in elasticsearch to the saved object client format. - * - * @param {RawDoc} rawDoc - The raw ES document to be converted to saved object format. - */ - public rawToSavedObject(doc: RawDoc): SanitizedSavedObjectDoc { - const { _id, _source, _seq_no, _primary_term } = doc; - const { type, namespace } = _source; - - const version = - _seq_no != null || _primary_term != null - ? encodeVersion(_seq_no!, _primary_term!) - : undefined; - - return { - type, - id: this.trimIdPrefix(namespace, type, _id), - ...(namespace && !this.schema.isNamespaceAgnostic(type) && { namespace }), - attributes: _source[type], - references: _source.references || [], - ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), - ...(_source.updated_at && { updated_at: _source.updated_at }), - ...(version && { version }), - }; - } - - /** - * Converts a document from the saved object client format to the format that is stored in elasticsearch. - * - * @param {SanitizedSavedObjectDoc} savedObj - The saved object to be converted to raw ES format. - */ - public savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): RawDoc { - const { - id, - type, - namespace, - attributes, - migrationVersion, - updated_at, - version, - references, - } = savedObj; - const source = { - [type]: attributes, - type, - references, - ...(namespace && !this.schema.isNamespaceAgnostic(type) && { namespace }), - ...(migrationVersion && { migrationVersion }), - ...(updated_at && { updated_at }), - }; - - return { - _id: this.generateRawId(namespace, type, id), - _source: source, - ...(version != null && decodeVersion(version)), - }; - } - - /** - * Given a saved object type and id, generates the compound id that is stored in the raw document. - * - * @param {string} namespace - The namespace of the saved object - * @param {string} type - The saved object type - * @param {string} id - The id of the saved object - */ - public generateRawId(namespace: string | undefined, type: string, id?: string) { - const namespacePrefix = - namespace && !this.schema.isNamespaceAgnostic(type) ? `${namespace}:` : ''; - return `${namespacePrefix}${type}:${id || uuid.v1()}`; - } - - private trimIdPrefix(namespace: string | undefined, type: string, id: string) { - assertNonEmptyString(id, 'document id'); - assertNonEmptyString(type, 'saved object type'); - - const namespacePrefix = - namespace && !this.schema.isNamespaceAgnostic(type) ? `${namespace}:` : ''; - const prefix = `${namespacePrefix}${type}:`; - - if (!id.startsWith(prefix)) { - return id; - } - - return id.slice(prefix.length); - } -} +export { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, SavedObjectsRawDoc } from './types'; +export { SavedObjectsSerializer } from './serializer'; diff --git a/src/core/server/saved_objects/serialization/serialization.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts similarity index 79% rename from src/core/server/saved_objects/serialization/serialization.test.ts rename to src/core/server/saved_objects/serialization/serializer.test.ts index e8bcdde73fcf4..8f09b25bb3908 100644 --- a/src/core/server/saved_objects/serialization/serialization.test.ts +++ b/src/core/server/saved_objects/serialization/serializer.test.ts @@ -18,14 +18,21 @@ */ import _ from 'lodash'; -import { SavedObjectsSerializer } from '.'; -import { SavedObjectsSchema } from '../schema'; +import { SavedObjectsSerializer } from './serializer'; +import { typeRegistryMock } from '../saved_objects_type_registry.mock'; import { encodeVersion } from '../version'; describe('saved object conversion', () => { + let typeRegistry: ReturnType; + + beforeEach(() => { + typeRegistry = typeRegistryMock.create(); + typeRegistry.isNamespaceAgnostic.mockReturnValue(false); + }); + describe('#rawToSavedObject', () => { test('it copies the _source.type property to type', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -36,7 +43,7 @@ describe('saved object conversion', () => { }); test('it copies the _source.references property to references', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -54,7 +61,7 @@ describe('saved object conversion', () => { }); test('if specified it copies the _source.migrationVersion property to migrationVersion', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -72,7 +79,7 @@ describe('saved object conversion', () => { }); test(`if _source.migrationVersion is unspecified it doesn't set migrationVersion`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -83,8 +90,8 @@ describe('saved object conversion', () => { }); test('it converts the id and type properties, and retains migrationVersion', () => { - const now = new Date(); - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const now = String(new Date()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'hello:world', _seq_no: 3, @@ -121,7 +128,7 @@ describe('saved object conversion', () => { }); test(`if version is unspecified it doesn't set version`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -133,7 +140,7 @@ describe('saved object conversion', () => { }); test(`if specified it encodes _seq_no and _primary_term to version`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _seq_no: 4, @@ -147,7 +154,7 @@ describe('saved object conversion', () => { }); test(`if only _seq_no is specified it throws`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect(() => serializer.rawToSavedObject({ _id: 'foo:bar', @@ -161,7 +168,7 @@ describe('saved object conversion', () => { }); test(`if only _primary_term is throws`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect(() => serializer.rawToSavedObject({ _id: 'foo:bar', @@ -175,7 +182,7 @@ describe('saved object conversion', () => { }); test('if specified it copies the _source.updated_at property to updated_at', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const now = Date(); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', @@ -188,7 +195,7 @@ describe('saved object conversion', () => { }); test(`if _source.updated_at is unspecified it doesn't set updated_at`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -199,7 +206,7 @@ describe('saved object conversion', () => { }); test('it does not pass unknown properties through', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'universe', _source: { @@ -221,7 +228,7 @@ describe('saved object conversion', () => { }); test('it does not create attributes if [type] is missing', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'universe', _source: { @@ -236,7 +243,7 @@ describe('saved object conversion', () => { }); test('it fails for documents which do not specify a type', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect(() => serializer.rawToSavedObject({ _id: 'universe', @@ -244,13 +251,13 @@ describe('saved object conversion', () => { hello: { world: 'earth', }, - }, + } as any, }) ).toThrow(/Expected "undefined" to be a saved object type/); }); test('it is complimentary with savedObjectToRaw', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const raw = { _id: 'foo-namespace:foo:bar', _primary_term: 24, @@ -266,7 +273,7 @@ describe('saved object conversion', () => { bar: '9.8.7', }, namespace: 'foo-namespace', - updated_at: new Date(), + updated_at: String(new Date()), references: [], }, }; @@ -277,7 +284,7 @@ describe('saved object conversion', () => { }); test('it handles unprefixed ids', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'universe', _source: { @@ -290,7 +297,7 @@ describe('saved object conversion', () => { describe('namespaced type without a namespace', () => { test(`removes type prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -302,7 +309,7 @@ describe('saved object conversion', () => { }); test(`if prefixed by random prefix and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'random:foo:bar', _source: { @@ -314,7 +321,7 @@ describe('saved object conversion', () => { }); test(`doesn't specify namespace`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -328,7 +335,7 @@ describe('saved object conversion', () => { describe('namespaced type with a namespace', () => { test(`removes type and namespace prefix from _id`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'baz:foo:bar', _source: { @@ -341,7 +348,7 @@ describe('saved object conversion', () => { }); test(`if prefixed by only type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -354,7 +361,7 @@ describe('saved object conversion', () => { }); test(`if prefixed by random prefix and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'random:foo:bar', _source: { @@ -367,7 +374,7 @@ describe('saved object conversion', () => { }); test(`copies _source.namespace to namespace`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'baz:foo:bar', _source: { @@ -381,10 +388,12 @@ describe('saved object conversion', () => { }); describe('namespace agnostic type with a namespace', () => { + beforeEach(() => { + typeRegistry.isNamespaceAgnostic.mockReturnValue(true); + }); + test(`removes type prefix from _id`, () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ foo: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'foo:bar', _source: { @@ -397,9 +406,7 @@ describe('saved object conversion', () => { }); test(`if prefixed by namespace and type it copies _id to id`, () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ foo: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'baz:foo:bar', _source: { @@ -412,9 +419,7 @@ describe('saved object conversion', () => { }); test(`doesn't copy _source.namespace to namespace`, () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ foo: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.rawToSavedObject({ _id: 'baz:foo:bar', _source: { @@ -430,7 +435,7 @@ describe('saved object conversion', () => { describe('#savedObjectToRaw', () => { test('it copies the type property to _source.type and uses the ROOT_TYPE as _type', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: 'foo', attributes: {}, @@ -440,7 +445,7 @@ describe('saved object conversion', () => { }); test('it copies the references property to _source.references', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ id: '1', type: 'foo', @@ -457,7 +462,7 @@ describe('saved object conversion', () => { }); test('if specified it copies the updated_at property to _source.updated_at', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const now = new Date(); const actual = serializer.savedObjectToRaw({ type: '', @@ -469,7 +474,7 @@ describe('saved object conversion', () => { }); test(`if unspecified it doesn't add updated_at property to _source`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, @@ -479,7 +484,7 @@ describe('saved object conversion', () => { }); test('it copies the migrationVersion property to _source.migrationVersion', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, @@ -496,7 +501,7 @@ describe('saved object conversion', () => { }); test(`if unspecified it doesn't add migrationVersion property to _source`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, @@ -506,7 +511,7 @@ describe('saved object conversion', () => { }); test('it decodes the version property to _seq_no and _primary_term', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, @@ -518,7 +523,7 @@ describe('saved object conversion', () => { }); test(`if unspecified it doesn't add _seq_no or _primary_term properties`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, @@ -529,7 +534,7 @@ describe('saved object conversion', () => { }); test(`if version invalid it throws`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect(() => serializer.savedObjectToRaw({ type: '', @@ -540,7 +545,7 @@ describe('saved object conversion', () => { }); test('it copies attributes to _source[type]', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: 'foo', attributes: { @@ -557,7 +562,7 @@ describe('saved object conversion', () => { describe('namespaced type without a namespace', () => { test('generates an id prefixed with type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const v1 = serializer.savedObjectToRaw({ type: 'foo', attributes: { @@ -577,7 +582,7 @@ describe('saved object conversion', () => { }); test(`doesn't specify _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: '', attributes: {}, @@ -589,7 +594,7 @@ describe('saved object conversion', () => { describe('namespaced type with a namespace', () => { test('generates an id prefixed with namespace and type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const v1 = serializer.savedObjectToRaw({ type: 'foo', namespace: 'bar', @@ -611,7 +616,7 @@ describe('saved object conversion', () => { }); test(`it copies namespace to _source.namespace`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: 'foo', attributes: {}, @@ -623,10 +628,12 @@ describe('saved object conversion', () => { }); describe('namespace agnostic type with a namespace', () => { + beforeEach(() => { + typeRegistry.isNamespaceAgnostic.mockReturnValue(true); + }); + test('generates an id prefixed with type, if no id is specified', () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ foo: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); const v1 = serializer.savedObjectToRaw({ type: 'foo', namespace: 'bar', @@ -648,9 +655,7 @@ describe('saved object conversion', () => { }); test(`doesn't specify _source.namespace`, () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ foo: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); const actual = serializer.savedObjectToRaw({ type: 'foo', namespace: 'bar', @@ -665,7 +670,7 @@ describe('saved object conversion', () => { describe('#isRawSavedObject', () => { describe('namespaced type without a namespace', () => { test('is true if the id is prefixed and the type matches', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -678,7 +683,7 @@ describe('saved object conversion', () => { }); test('is false if the id is not prefixed', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'world', @@ -691,19 +696,19 @@ describe('saved object conversion', () => { }); test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', _source: { hello: {}, - }, + } as any, }) ).toBeFalsy(); }); test(`is false if the type prefix omits the :`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'helloworld', @@ -716,7 +721,7 @@ describe('saved object conversion', () => { }); test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -730,7 +735,7 @@ describe('saved object conversion', () => { }); test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -745,7 +750,7 @@ describe('saved object conversion', () => { describe('namespaced type with a namespace', () => { test('is true if the id is prefixed with type and namespace and the type matches', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'foo:hello:world', @@ -759,7 +764,7 @@ describe('saved object conversion', () => { }); test('is false if the id is not prefixed by anything', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'world', @@ -773,7 +778,7 @@ describe('saved object conversion', () => { }); test('is false if the id is prefixed only with type and the type matches', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -787,7 +792,7 @@ describe('saved object conversion', () => { }); test('is false if the id is prefixed only with namespace and the namespace matches', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'foo:world', @@ -801,7 +806,7 @@ describe('saved object conversion', () => { }); test(`is false if the id prefix omits the trailing :`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'foo:helloworld', @@ -815,20 +820,20 @@ describe('saved object conversion', () => { }); test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'foo:hello:world', _source: { hello: {}, namespace: 'foo', - }, + } as any, }) ).toBeFalsy(); }); test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'foo:hello:world', @@ -843,7 +848,7 @@ describe('saved object conversion', () => { }); test('is false if the namespace attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'bar:jam:world', @@ -858,7 +863,7 @@ describe('saved object conversion', () => { }); test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -872,11 +877,13 @@ describe('saved object conversion', () => { }); }); - describe('namespace agonstic type with a namespace', () => { + describe('namespace agnostic type with a namespace', () => { + beforeEach(() => { + typeRegistry.isNamespaceAgnostic.mockReturnValue(true); + }); + test('is true if the id is prefixed with type and the type matches', () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ hello: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -890,9 +897,7 @@ describe('saved object conversion', () => { }); test('is false if the id is not prefixed', () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ hello: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'world', @@ -906,9 +911,7 @@ describe('saved object conversion', () => { }); test('is false if the id is prefixed with type and namespace', () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ hello: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'foo:hello:world', @@ -922,9 +925,7 @@ describe('saved object conversion', () => { }); test(`is false if the type prefix omits the :`, () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ hello: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'helloworld', @@ -938,24 +939,20 @@ describe('saved object conversion', () => { }); test('is false if the type attribute is missing', () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ hello: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', _source: { hello: {}, namespace: 'foo', - }, + } as any, }) ).toBeFalsy(); }); test('is false if the type attribute does not match the id', () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ hello: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -970,9 +967,7 @@ describe('saved object conversion', () => { }); test('is false if there is no [type] attribute', () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ hello: { isNamespaceAgnostic: true } }) - ); + const serializer = new SavedObjectsSerializer(typeRegistry); expect( serializer.isRawSavedObject({ _id: 'hello:world', @@ -990,13 +985,13 @@ describe('saved object conversion', () => { describe('#generateRawId', () => { describe('namespaced type without a namespace', () => { test('generates an id if none is specified', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const id = serializer.generateRawId('', 'goodbye'); expect(id).toMatch(/goodbye\:[\w-]+$/); }); test('uses the id that is specified', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const id = serializer.generateRawId('', 'hello', 'world'); expect(id).toMatch('hello:world'); }); @@ -1004,13 +999,13 @@ describe('saved object conversion', () => { describe('namespaced type with a namespace', () => { test('generates an id if none is specified and prefixes namespace', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const id = serializer.generateRawId('foo', 'goodbye'); expect(id).toMatch(/foo:goodbye\:[\w-]+$/); }); test('uses the id that is specified and prefixes the namespace', () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const id = serializer.generateRawId('foo', 'hello', 'world'); expect(id).toMatch('foo:hello:world'); }); @@ -1018,15 +1013,15 @@ describe('saved object conversion', () => { describe('namespace agnostic type with a namespace', () => { test(`generates an id if none is specified and doesn't prefix namespace`, () => { - const serializer = new SavedObjectsSerializer( - new SavedObjectsSchema({ foo: { isNamespaceAgnostic: true } }) - ); + typeRegistry.isNamespaceAgnostic.mockReturnValue(true); + + const serializer = new SavedObjectsSerializer(typeRegistry); const id = serializer.generateRawId('foo', 'goodbye'); expect(id).toMatch(/goodbye\:[\w-]+$/); }); test(`uses the id that is specified and doesn't prefix the namespace`, () => { - const serializer = new SavedObjectsSerializer(new SavedObjectsSchema()); + const serializer = new SavedObjectsSerializer(typeRegistry); const id = serializer.generateRawId('foo', 'hello', 'world'); expect(id).toMatch('hello:world'); }); diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts new file mode 100644 index 0000000000000..99d6b0c6b59f9 --- /dev/null +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -0,0 +1,151 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import uuid from 'uuid'; +import { decodeVersion, encodeVersion } from '../version'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; +import { SavedObjectsRawDoc, SavedObjectSanitizedDoc } from './types'; + +/** + * A serializer that can be used to manually convert {@link SavedObjectsRawDoc | raw} or + * {@link SavedObjectSanitizedDoc | sanitized} documents to the other kind. + * + * @remarks Serializer instances should only be created and accessed by calling {@link SavedObjectsServiceStart.createSerializer} + * + * @public + */ +export class SavedObjectsSerializer { + private readonly registry: ISavedObjectTypeRegistry; + + /** + * @internal + */ + constructor(registry: ISavedObjectTypeRegistry) { + this.registry = registry; + } + /** + * Determines whether or not the raw document can be converted to a saved object. + * + * @param {SavedObjectsRawDoc} rawDoc - The raw ES document to be tested + */ + public isRawSavedObject(rawDoc: SavedObjectsRawDoc) { + const { type, namespace } = rawDoc._source; + const namespacePrefix = + namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + return Boolean( + type && + rawDoc._id.startsWith(`${namespacePrefix}${type}:`) && + rawDoc._source.hasOwnProperty(type) + ); + } + + /** + * Converts a document from the format that is stored in elasticsearch to the saved object client format. + * + * @param {SavedObjectsRawDoc} doc - The raw ES document to be converted to saved object format. + */ + public rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc { + const { _id, _source, _seq_no, _primary_term } = doc; + const { type, namespace } = _source; + + const version = + _seq_no != null || _primary_term != null + ? encodeVersion(_seq_no!, _primary_term!) + : undefined; + + return { + type, + id: this.trimIdPrefix(namespace, type, _id), + ...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }), + attributes: _source[type], + references: _source.references || [], + ...(_source.migrationVersion && { migrationVersion: _source.migrationVersion }), + ...(_source.updated_at && { updated_at: _source.updated_at }), + ...(version && { version }), + }; + } + + /** + * Converts a document from the saved object client format to the format that is stored in elasticsearch. + * + * @param {SavedObjectSanitizedDoc} savedObj - The saved object to be converted to raw ES format. + */ + public savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc { + const { + id, + type, + namespace, + attributes, + migrationVersion, + updated_at, + version, + references, + } = savedObj; + const source = { + [type]: attributes, + type, + references, + ...(namespace && !this.registry.isNamespaceAgnostic(type) && { namespace }), + ...(migrationVersion && { migrationVersion }), + ...(updated_at && { updated_at }), + }; + + return { + _id: this.generateRawId(namespace, type, id), + _source: source, + ...(version != null && decodeVersion(version)), + }; + } + + /** + * Given a saved object type and id, generates the compound id that is stored in the raw document. + * + * @param {string} namespace - The namespace of the saved object + * @param {string} type - The saved object type + * @param {string} id - The id of the saved object + */ + public generateRawId(namespace: string | undefined, type: string, id?: string) { + const namespacePrefix = + namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + return `${namespacePrefix}${type}:${id || uuid.v1()}`; + } + + private trimIdPrefix(namespace: string | undefined, type: string, id: string) { + assertNonEmptyString(id, 'document id'); + assertNonEmptyString(type, 'saved object type'); + + const namespacePrefix = + namespace && !this.registry.isNamespaceAgnostic(type) ? `${namespace}:` : ''; + const prefix = `${namespacePrefix}${type}:`; + + if (!id.startsWith(prefix)) { + return id; + } + + return id.slice(prefix.length); + } +} + +function assertNonEmptyString(value: string, name: string) { + if (!value || typeof value !== 'string') { + throw new TypeError(`Expected "${value}" to be a ${name}`); + } +} diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts new file mode 100644 index 0000000000000..aaf6f45c244ec --- /dev/null +++ b/src/core/server/saved_objects/serialization/types.ts @@ -0,0 +1,79 @@ +/* + * 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 { SavedObjectsMigrationVersion, SavedObjectReference } from '../types'; + +/** + * A raw document as represented directly in the saved object index. + * + * @public + */ +export interface SavedObjectsRawDoc { + _id: string; + _source: SavedObjectsRawDocSource; + _type?: string; + _seq_no?: number; + _primary_term?: number; +} + +/** @public */ +export interface SavedObjectsRawDocSource { + type: string; + namespace?: string; + migrationVersion?: SavedObjectsMigrationVersion; + updated_at?: string; + references?: SavedObjectReference[]; + + [typeMapping: string]: any; +} + +/** + * A saved object type definition that allows for miscellaneous, unknown + * properties, as current discussions around security, ACLs, etc indicate + * that future props are likely to be added. Migrations support this + * scenario out of the box. + */ +interface SavedObjectDoc { + attributes: object; + id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional + type: string; + namespace?: string; + migrationVersion?: SavedObjectsMigrationVersion; + version?: string; + updated_at?: string; + + [rootProp: string]: any; +} + +interface Referencable { + references: SavedObjectReference[]; +} + +/** + * We want to have two types, one that guarantees a "references" attribute + * will exist and one that allows it to be null. Since we're not migrating + * all the saved objects to have a "references" array, we need to support + * the scenarios where it may be missing (ex migrations). + * + * @public + */ +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; + +/** @public */ +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; 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 6171faa7a617c..2e5eeec04e0a8 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -21,10 +21,9 @@ import _ from 'lodash'; import { SavedObjectsRepository } from './repository'; import * as getSearchDslNS from './search_dsl/search_dsl'; import { SavedObjectsErrorHelpers } from './errors'; -import { SavedObjectsSchema } from '../../schema'; import { SavedObjectsSerializer } from '../../serialization'; -import { getRootPropertiesObjects } from '../../mappings/lib/get_root_properties_objects'; import { encodeHitVersion } from '../../version'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; jest.mock('./search_dsl/search_dsl', () => ({ getSearchDsl: jest.fn() })); @@ -35,6 +34,7 @@ describe('SavedObjectsRepository', () => { let callAdminCluster; let savedObjectsRepository; let migrator; + const mockTimestamp = '2017-08-14T15:49:14.886Z'; const mockTimestampFields = { updated_at: mockTimestamp }; const mockVersionProps = { _seq_no: 1, _primary_term: 1 }; @@ -240,12 +240,95 @@ describe('SavedObjectsRepository', () => { }, }; - const schema = new SavedObjectsSchema({ - globaltype: { isNamespaceAgnostic: true }, - foo: { isNamespaceAgnostic: true }, - bar: { isNamespaceAgnostic: true }, - baz: { indexPattern: 'beats' }, - hiddenType: { isNamespaceAgnostic: true, hidden: true }, + const typeRegistry = new SavedObjectTypeRegistry(); + typeRegistry.registerType({ + name: 'config', + hidden: false, + namespaceAgnostic: false, + mappings: { + properties: { + type: 'keyword', + }, + }, + }); + typeRegistry.registerType({ + name: 'index-pattern', + hidden: false, + namespaceAgnostic: false, + mappings: { + properties: { + someField: { + type: 'keyword', + }, + }, + }, + }); + typeRegistry.registerType({ + name: 'dashboard', + hidden: false, + namespaceAgnostic: false, + mappings: { + properties: { + otherField: { + type: 'keyword', + }, + }, + }, + }); + typeRegistry.registerType({ + name: 'globaltype', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + yetAnotherField: { + type: 'keyword', + }, + }, + }, + }); + typeRegistry.registerType({ + name: 'foo', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + type: 'keyword', + }, + }, + }); + typeRegistry.registerType({ + name: 'bar', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + type: 'keyword', + }, + }, + }); + typeRegistry.registerType({ + name: 'baz', + hidden: false, + namespaceAgnostic: false, + indexPattern: 'beats', + mappings: { + properties: { + type: 'keyword', + }, + }, + }); + typeRegistry.registerType({ + name: 'hiddenType', + hidden: true, + namespaceAgnostic: true, + mappings: { + properties: { + someField: { + type: 'keyword', + }, + }, + }, }); beforeEach(() => { @@ -255,16 +338,16 @@ describe('SavedObjectsRepository', () => { runMigrations: async () => ({ status: 'skipped' }), }; - const serializer = new SavedObjectsSerializer(schema); - const allTypes = Object.keys(getRootPropertiesObjects(mappings)); - const allowedTypes = [...new Set(allTypes.filter(type => !schema.isHiddenType(type)))]; + const serializer = new SavedObjectsSerializer(typeRegistry); + const allTypes = typeRegistry.getAllTypes().map(type => type.name); + const allowedTypes = [...new Set(allTypes.filter(type => !typeRegistry.isHidden(type)))]; savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', mappings, callCluster: callAdminCluster, migrator, - schema, + typeRegistry, serializer, allowedTypes, }); @@ -1171,7 +1254,7 @@ describe('SavedObjectsRepository', () => { expect(result).toEqual(deleteByQueryResults); expect(callAdminCluster).toHaveBeenCalledTimes(1); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, schema, { + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, typeRegistry, { namespace: 'my-namespace', type: ['config', 'baz', 'index-pattern', 'dashboard'], }); @@ -1261,7 +1344,11 @@ describe('SavedObjectsRepository', () => { await savedObjectsRepository.find(relevantOpts); expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, schema, relevantOpts); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith( + mappings, + typeRegistry, + relevantOpts + ); }); it('accepts KQL filter and passes keuryNode to getSearchDsl', async () => { diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 70b8b2878c15b..b485b8dfe398c 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -27,9 +27,12 @@ import { includedFields } from './included_fields'; import { decorateEsError } from './decorate_es_error'; import { SavedObjectsErrorHelpers } from './errors'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; -import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; -import { SavedObjectsSerializer, SanitizedSavedObjectDoc, RawDoc } from '../../serialization'; +import { + SavedObjectsSerializer, + SavedObjectSanitizedDoc, + SavedObjectsRawDoc, +} from '../../serialization'; import { SavedObjectsBulkCreateObject, SavedObjectsBulkGetObject, @@ -51,8 +54,8 @@ import { SavedObjectsMigrationVersion, MutatingOperationRefreshSetting, } from '../../types'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; -import { LegacyConfig } from '../../../legacy'; // 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. @@ -75,11 +78,9 @@ const isLeft = (either: Either): either is Left => { export interface SavedObjectsRepositoryOptions { index: string; - /** @deprecated Will be removed once SavedObjectsSchema is exposed from Core */ - config: LegacyConfig; mappings: IndexMapping; callCluster: APICaller; - schema: SavedObjectsSchema; + typeRegistry: SavedObjectTypeRegistry; serializer: SavedObjectsSerializer; migrator: KibanaMigrator; allowedTypes: string[]; @@ -118,9 +119,8 @@ export type ISavedObjectsRepository = Pick !schema.isHiddenType(type)); + const serializer = new SavedObjectsSerializer(typeRegistry); + const visibleTypes = allTypes.filter(type => !typeRegistry.isHidden(type)); const missingTypeMappings = extraTypes.filter(type => !allTypes.includes(type)); if (missingTypeMappings.length > 0) { @@ -158,10 +157,9 @@ export class SavedObjectsRepository { return new injectedConstructor({ index: indexName, - config, migrator, mappings, - schema, + typeRegistry, serializer, allowedTypes, callCluster: retryCallCluster(callCluster), @@ -171,10 +169,9 @@ export class SavedObjectsRepository { private constructor(options: SavedObjectsRepositoryOptions) { const { index, - config, mappings, callCluster, - schema, + typeRegistry, serializer, migrator, allowedTypes = [], @@ -189,9 +186,8 @@ export class SavedObjectsRepository { // to returning them. this._migrator = migrator; this._index = index; - this._config = config; this._mappings = mappings; - this._schema = schema; + this._registry = typeRegistry; if (allowedTypes.length === 0) { throw new Error('Empty or missing types for saved object repository!'); } @@ -201,7 +197,6 @@ export class SavedObjectsRepository { await migrator.runMigrations(); return callCluster(...args); }; - this._schema = schema; this._serializer = serializer; } @@ -250,7 +245,7 @@ export class SavedObjectsRepository { references, }); - const raw = this._serializer.savedObjectToRaw(migrated as SanitizedSavedObjectDoc); + const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); const response = await this._writeToCluster(method, { id: raw._id, @@ -316,7 +311,7 @@ export class SavedObjectsRepository { namespace, updated_at: time, references: object.references || [], - }) as SanitizedSavedObjectDoc + }) as SavedObjectSanitizedDoc ), }; @@ -435,7 +430,7 @@ export class SavedObjectsRepository { const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); - const typesToDelete = allTypes.filter(type => !this._schema.isNamespaceAgnostic(type)); + const typesToDelete = allTypes.filter(type => !this._registry.isNamespaceAgnostic(type)); const esOptions = { index: this.getIndicesForTypes(typesToDelete), @@ -443,7 +438,7 @@ export class SavedObjectsRepository { refresh, body: { conflicts: 'proceed', - ...getSearchDsl(this._mappings, this._schema, { + ...getSearchDsl(this._mappings, this._registry, { namespace, type: typesToDelete, }), @@ -531,7 +526,7 @@ export class SavedObjectsRepository { rest_total_hits_as_int: true, body: { seq_no_primary_term: true, - ...getSearchDsl(this._mappings, this._schema, { + ...getSearchDsl(this._mappings, this._registry, { search, defaultSearchOperator, searchFields, @@ -562,7 +557,9 @@ export class SavedObjectsRepository { page, per_page: perPage, total: response.hits.total, - saved_objects: response.hits.hits.map((hit: RawDoc) => this._rawToSavedObject(hit)), + saved_objects: response.hits.hits.map((hit: SavedObjectsRawDoc) => + this._rawToSavedObject(hit) + ), }; } @@ -890,7 +887,7 @@ export class SavedObjectsRepository { updated_at: time, }); - const raw = this._serializer.savedObjectToRaw(migrated as SanitizedSavedObjectDoc); + const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); const response = await this._writeToCluster('update', { id: this._serializer.generateRawId(namespace, type, id), @@ -952,7 +949,7 @@ export class SavedObjectsRepository { * @param type - the type */ private getIndexForType(type: string) { - return this._schema.getIndexForType(this._config, type) || this._index; + return this._registry.getIndex(type) || this._index; } /** @@ -964,7 +961,7 @@ export class SavedObjectsRepository { */ private getIndicesForTypes(types: string[]) { const unique = (array: string[]) => [...new Set(array)]; - return unique(types.map(t => this._schema.getIndexForType(this._config, t) || this._index)); + return unique(types.map(t => this.getIndexForType(t))); } private _getCurrentTime() { @@ -975,7 +972,7 @@ export class SavedObjectsRepository { // includes the namespace, and we use this for migrating documents. However, we don't // 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 { + private _rawToSavedObject(raw: SavedObjectsRawDoc): SavedObject { const savedObject = this._serializer.rawToSavedObject(raw); return omit(savedObject, 'namespace'); } diff --git a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts index b3ea056484760..4a87bb1043ca2 100644 --- a/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository_create_repository.test.ts @@ -18,43 +18,54 @@ */ import { SavedObjectsRepository } from './repository'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; -import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; -import { LegacyConfig } from '../../../legacy'; +import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; + jest.mock('./repository'); const { SavedObjectsRepository: originalRepository } = jest.requireActual('./repository'); describe('SavedObjectsRepository#createRepository', () => { const callAdminCluster = jest.fn(); - const schema = new SavedObjectsSchema({ - nsAgnosticType: { isNamespaceAgnostic: true }, - nsType: { indexPattern: 'beats', isNamespaceAgnostic: false }, - hiddenType: { isNamespaceAgnostic: true, hidden: true }, + + const typeRegistry = new SavedObjectTypeRegistry(); + typeRegistry.registerType({ + name: 'nsAgnosticType', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + name: { type: 'keyword' }, + }, + }, + migrations: {}, + }); + + typeRegistry.registerType({ + name: 'nsType', + hidden: false, + namespaceAgnostic: false, + indexPattern: 'beats', + mappings: { + properties: { + name: { type: 'keyword' }, + }, + }, + migrations: {}, }); - const mappings = [ - { - pluginId: 'testplugin', + typeRegistry.registerType({ + name: 'hiddenType', + hidden: true, + namespaceAgnostic: true, + mappings: { properties: { - nsAgnosticType: { - properties: { - name: { type: 'keyword' }, - }, - }, - nsType: { - properties: { - name: { type: 'keyword' }, - }, - }, - hiddenType: { - properties: { - name: { type: 'keyword' }, - }, - }, + name: { type: 'keyword' }, }, }, - ]; - const migrator = mockKibanaMigrator.create({ savedObjectMappings: mappings }); + migrations: {}, + }); + + const migrator = mockKibanaMigrator.create({ types: typeRegistry.getAllTypes() }); const RepositoryConstructor = (SavedObjectsRepository as unknown) as jest.Mock< SavedObjectsRepository >; @@ -67,8 +78,7 @@ describe('SavedObjectsRepository#createRepository', () => { try { originalRepository.createRepository( (migrator as unknown) as KibanaMigrator, - schema, - {} as LegacyConfig, + typeRegistry, '.kibana-test', callAdminCluster, ['unMappedType1', 'unmappedType2'] @@ -83,8 +93,7 @@ describe('SavedObjectsRepository#createRepository', () => { it('should create a repository without hidden types', () => { const repository = originalRepository.createRepository( (migrator as unknown) as KibanaMigrator, - schema, - {} as LegacyConfig, + typeRegistry, '.kibana-test', callAdminCluster, [], @@ -103,8 +112,7 @@ describe('SavedObjectsRepository#createRepository', () => { it('should create a repository with a unique list of hidden types', () => { const repository = originalRepository.createRepository( (migrator as unknown) as KibanaMigrator, - schema, - {} as LegacyConfig, + typeRegistry, '.kibana-test', callAdminCluster, ['hiddenType', 'hiddenType', 'hiddenType'], 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 e585953109d2c..b2129765ee426 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 @@ -17,10 +17,11 @@ * under the License. */ -import { schemaMock } from '../../../schema/schema.mock'; +import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; import { getQueryParams } from './query_params'; -const SCHEMA = schemaMock.create(); +const registry = typeRegistryMock.create(); + const MAPPINGS = { properties: { type: { @@ -85,7 +86,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: MAPPINGS, schema: SCHEMA })).toEqual({ + expect(getQueryParams({ mappings: MAPPINGS, registry })).toEqual({ query: { bool: { filter: [ @@ -108,9 +109,7 @@ describe('searchDsl/queryParams', () => { describe('namespace', () => { it('filters namespaced types for namespace, and ensures namespace agnostic types have no namespace', () => { - expect( - getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: 'foo-namespace' }) - ).toEqual({ + expect(getQueryParams({ mappings: MAPPINGS, registry, namespace: 'foo-namespace' })).toEqual({ query: { bool: { filter: [ @@ -134,7 +133,7 @@ describe('searchDsl/queryParams', () => { describe('type (singular, namespaced)', () => { it('includes a terms filter for type and namespace not being specified', () => { expect( - getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'saved' }) + getQueryParams({ mappings: MAPPINGS, registry, namespace: undefined, type: 'saved' }) ).toEqual({ query: { bool: { @@ -155,7 +154,7 @@ describe('searchDsl/queryParams', () => { describe('type (singular, global)', () => { it('includes a terms filter for type and namespace not being specified', () => { expect( - getQueryParams({ mappings: MAPPINGS, schema: SCHEMA, namespace: undefined, type: 'global' }) + getQueryParams({ mappings: MAPPINGS, registry, namespace: undefined, type: 'global' }) ).toEqual({ query: { bool: { @@ -178,7 +177,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: ['saved', 'global'], }) @@ -204,7 +203,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: ['saved', 'global'], }) @@ -230,7 +229,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: undefined, search: 'us*', @@ -270,7 +269,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: undefined, search: 'us*', @@ -310,7 +309,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: ['saved', 'global'], search: 'us*', @@ -346,7 +345,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: ['saved', 'global'], search: 'us*', @@ -382,7 +381,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: undefined, search: 'y*', @@ -419,7 +418,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: undefined, search: 'y*', @@ -456,7 +455,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: undefined, search: 'y*', @@ -503,7 +502,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: undefined, search: 'y*', @@ -540,7 +539,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: undefined, search: 'y*', @@ -577,7 +576,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: undefined, search: 'y*', @@ -624,7 +623,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: ['saved', 'global'], search: 'y*', @@ -657,7 +656,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: ['saved', 'global'], search: 'y*', @@ -690,7 +689,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: undefined, type: ['saved', 'global'], search: 'y*', @@ -726,7 +725,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: ['saved', 'global'], search: 'y*', @@ -759,7 +758,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: ['saved', 'global'], search: 'y*', @@ -792,7 +791,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: ['saved', 'global'], search: 'y*', @@ -828,7 +827,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: ['saved', 'global'], search: 'foo', @@ -902,7 +901,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', type: ['saved', 'global'], search: undefined, @@ -958,7 +957,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', kueryNode: { type: 'function', @@ -1052,7 +1051,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', kueryNode: { type: 'function', @@ -1189,7 +1188,7 @@ describe('searchDsl/queryParams', () => { expect( getQueryParams({ mappings: MAPPINGS, - schema: SCHEMA, + registry, namespace: 'foo-namespace', search: 'y*', searchFields: ['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 a1e3ae9620299..3fabad6af08ff 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 @@ -20,7 +20,7 @@ import { esKuery } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; -import { SavedObjectsSchema } from '../../../schema'; +import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; /** * Gets the types based on the type. Uses mappings to support @@ -61,8 +61,12 @@ function getFieldsForTypes(types: string[], searchFields?: string[]) { * Gets the clause that will filter for the type in the namespace. * Some types are namespace agnostic, so they must be treated differently. */ -function getClauseForType(schema: SavedObjectsSchema, namespace: string | undefined, type: string) { - if (namespace && !schema.isNamespaceAgnostic(type)) { +function getClauseForType( + registry: ISavedObjectTypeRegistry, + namespace: string | undefined, + type: string +) { + if (namespace && !registry.isNamespaceAgnostic(type)) { return { bool: { must: [{ term: { type } }, { term: { namespace } }], @@ -85,7 +89,7 @@ interface HasReferenceQueryParams { interface QueryParams { mappings: IndexMapping; - schema: SavedObjectsSchema; + registry: ISavedObjectTypeRegistry; namespace?: string; type?: string | string[]; search?: string; @@ -100,7 +104,7 @@ interface QueryParams { */ export function getQueryParams({ mappings, - schema, + registry, namespace, type, search, @@ -140,7 +144,7 @@ export function getQueryParams({ }, ] : undefined, - should: types.map(shouldType => getClauseForType(schema, namespace, shouldType)), + should: types.map(shouldType => getClauseForType(registry, namespace, shouldType)), minimum_should_match: 1, }, }, 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 97cab3e566d5e..95b7ffd117ee9 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 @@ -20,7 +20,7 @@ jest.mock('./query_params'); jest.mock('./sorting_params'); -import { schemaMock } from '../../../schema/schema.mock'; +import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; import * as queryParamsNS from './query_params'; import { getSearchDsl } from './search_dsl'; import * as sortParamsNS from './sorting_params'; @@ -28,8 +28,8 @@ import * as sortParamsNS from './sorting_params'; const getQueryParams = queryParamsNS.getQueryParams as jest.Mock; const getSortingParams = sortParamsNS.getSortingParams as jest.Mock; -const SCHEMA = schemaMock.create(); -const MAPPINGS = { properties: {} }; +const registry = typeRegistryMock.create(); +const mappings = { properties: {} }; describe('getSearchDsl', () => { afterEach(() => { @@ -40,7 +40,7 @@ describe('getSearchDsl', () => { describe('validation', () => { it('throws when type is not specified', () => { expect(() => { - getSearchDsl(MAPPINGS, SCHEMA, { + getSearchDsl(mappings, registry, { type: undefined as any, sortField: 'title', }); @@ -48,7 +48,7 @@ describe('getSearchDsl', () => { }); it('throws when sortOrder without sortField', () => { expect(() => { - getSearchDsl(MAPPINGS, SCHEMA, { + getSearchDsl(mappings, registry, { type: 'foo', sortOrder: 'desc', }); @@ -70,11 +70,11 @@ describe('getSearchDsl', () => { }, }; - getSearchDsl(MAPPINGS, SCHEMA, opts); + getSearchDsl(mappings, registry, opts); expect(getQueryParams).toHaveBeenCalledTimes(1); expect(getQueryParams).toHaveBeenCalledWith({ - mappings: MAPPINGS, - schema: SCHEMA, + mappings, + registry, namespace: opts.namespace, type: opts.type, search: opts.search, @@ -92,10 +92,10 @@ describe('getSearchDsl', () => { sortOrder: 'baz', }; - getSearchDsl(MAPPINGS, SCHEMA, opts); + getSearchDsl(mappings, registry, opts); expect(getSortingParams).toHaveBeenCalledTimes(1); expect(getSortingParams).toHaveBeenCalledWith( - MAPPINGS, + mappings, opts.type, opts.sortField, opts.sortOrder @@ -105,7 +105,7 @@ describe('getSearchDsl', () => { it('returns combination of getQueryParams and getSortingParams', () => { getQueryParams.mockReturnValue({ a: 'a' }); getSortingParams.mockReturnValue({ b: 'b' }); - expect(getSearchDsl(MAPPINGS, SCHEMA, { type: 'foo' })).toEqual({ a: 'a', b: 'b' }); + expect(getSearchDsl(mappings, registry, { type: 'foo' })).toEqual({ a: 'a', b: 'b' }); }); }); }); 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 1b6e1361bb92a..75ab058a38be9 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 @@ -20,11 +20,11 @@ import Boom from 'boom'; import { IndexMapping } from '../../../mappings'; -import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { esKuery } from '../../../../../../plugins/data/server'; +import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; interface GetSearchDslOptions { type: string | string[]; @@ -43,7 +43,7 @@ interface GetSearchDslOptions { export function getSearchDsl( mappings: IndexMapping, - schema: SavedObjectsSchema, + registry: ISavedObjectTypeRegistry, options: GetSearchDslOptions ) { const { @@ -69,7 +69,7 @@ export function getSearchDsl( return { ...getQueryParams({ mappings, - schema, + registry, namespace, type, search, diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index a3fe2b937635b..980ba005e0eeb 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -18,9 +18,8 @@ */ import { SavedObjectsClient } from './service/saved_objects_client'; -import { SavedObjectsMapping } from './mappings'; -import { MigrationDefinition } from './migrations/core/document_migrator'; -import { SavedObjectsSchemaDefinition } from './schema'; +import { SavedObjectsTypeMappingDefinition, SavedObjectsTypeMappingDefinitions } from './mappings'; +import { SavedObjectMigrationMap } from './migrations'; import { PropertyValidators } from './validation'; export { @@ -34,6 +33,7 @@ export { } from './import/types'; import { SavedObjectAttributes } from '../../types'; +import { LegacyConfig } from '../legacy'; export { SavedObjectAttributes, SavedObjectAttribute, @@ -208,13 +208,88 @@ export type MutatingOperationRefreshSetting = boolean | 'wait_for'; */ export type SavedObjectsClientContract = Pick; +/** + * @remarks This is only internal for now, and will only be public when we expose the registerType API + * + * @public + */ +export interface SavedObjectsType { + /** + * The name of the type, which is also used as the internal id. + */ + name: string; + /** + * Is the type hidden by default. If true, repositories will not have access to this type unless explicitly + * declared as an `extraType` when creating the repository. + * + * See {@link SavedObjectsServiceStart.createInternalRepository | createInternalRepository}. + */ + hidden: boolean; + /** + * Is the type global (true), or namespaced (false). + */ + namespaceAgnostic: boolean; + /** + * If defined, the type instances will be stored in the given index instead of the default one. + */ + indexPattern?: string; + /** + * If defined, will be used to convert the type to an alias. + */ + convertToAliasScript?: string; + /** + * The {@link SavedObjectsTypeMappingDefinition | mapping definition} for the type. + */ + mappings: SavedObjectsTypeMappingDefinition; + /** + * An optional map of {@link SavedObjectMigrationFn | migrations} to be used to migrate the type. + */ + migrations?: SavedObjectMigrationMap; +} + /** * @internal * @deprecated */ export interface SavedObjectsLegacyUiExports { - savedObjectMappings: SavedObjectsMapping[]; - savedObjectMigrations: MigrationDefinition; - savedObjectSchemas: SavedObjectsSchemaDefinition; + savedObjectMappings: SavedObjectsLegacyMapping[]; + savedObjectMigrations: SavedObjectsLegacyMigrationDefinitions; + savedObjectSchemas: SavedObjectsLegacySchemaDefinitions; savedObjectValidations: PropertyValidators; } + +/** + * @internal + * @deprecated + */ +export interface SavedObjectsLegacyMapping { + pluginId: string; + properties: SavedObjectsTypeMappingDefinitions; +} + +/** + * @internal + * @deprecated + */ +export interface SavedObjectsLegacyMigrationDefinitions { + [type: string]: SavedObjectMigrationMap; +} + +/** + * @internal + * @deprecated + */ +interface SavedObjectsLegacyTypeSchema { + isNamespaceAgnostic?: boolean; + hidden?: boolean; + indexPattern?: ((config: LegacyConfig) => string) | string; + convertToAliasScript?: string; +} + +/** + * @internal + * @deprecated + */ +export interface SavedObjectsLegacySchemaDefinitions { + [type: string]: SavedObjectsLegacyTypeSchema; +} diff --git a/src/core/server/saved_objects/utils.test.ts b/src/core/server/saved_objects/utils.test.ts new file mode 100644 index 0000000000000..d1c15517e94a6 --- /dev/null +++ b/src/core/server/saved_objects/utils.test.ts @@ -0,0 +1,285 @@ +/* + * 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 { legacyServiceMock } from '../legacy/legacy_service.mock'; +import { convertLegacyTypes, convertTypesToLegacySchema } from './utils'; +import { SavedObjectsLegacyUiExports, SavedObjectsType } from './types'; +import { LegacyConfig } from 'kibana/server'; + +describe('convertLegacyTypes', () => { + let legacyConfig: ReturnType; + + beforeEach(() => { + legacyConfig = legacyServiceMock.createLegacyConfig(); + }); + + it('converts the legacy mappings using default values if no schemas are specified', () => { + const uiExports: SavedObjectsLegacyUiExports = { + savedObjectMappings: [ + { + pluginId: 'pluginA', + properties: { + typeA: { + properties: { + fieldA: { type: 'text' }, + }, + }, + typeB: { + properties: { + fieldB: { type: 'text' }, + }, + }, + }, + }, + { + pluginId: 'pluginB', + properties: { + typeC: { + properties: { + fieldC: { type: 'text' }, + }, + }, + }, + }, + ], + savedObjectMigrations: {}, + savedObjectSchemas: {}, + savedObjectValidations: {}, + }; + + const converted = convertLegacyTypes(uiExports, legacyConfig); + expect(converted).toMatchSnapshot(); + }); + + it('merges the mappings and the schema to create the type when schema exists for the type', () => { + const uiExports: SavedObjectsLegacyUiExports = { + savedObjectMappings: [ + { + pluginId: 'pluginA', + properties: { + typeA: { + properties: { + fieldA: { type: 'text' }, + }, + }, + }, + }, + { + pluginId: 'pluginB', + properties: { + typeC: { + properties: { + fieldC: { type: 'text' }, + }, + }, + }, + }, + ], + savedObjectMigrations: {}, + savedObjectSchemas: { + typeA: { + indexPattern: 'fooBar', + hidden: true, + isNamespaceAgnostic: true, + }, + }, + savedObjectValidations: {}, + }; + + const converted = convertLegacyTypes(uiExports, legacyConfig); + expect(converted).toMatchSnapshot(); + }); + + it('invokes indexPattern to retrieve the index when it is a function', () => { + const indexPatternAccessor: (config: LegacyConfig) => string = jest.fn(config => { + config.get('foo.bar'); + return 'myIndex'; + }); + + const uiExports: SavedObjectsLegacyUiExports = { + savedObjectMappings: [ + { + pluginId: 'pluginA', + properties: { + typeA: { + properties: { + fieldA: { type: 'text' }, + }, + }, + }, + }, + ], + savedObjectMigrations: {}, + savedObjectSchemas: { + typeA: { + indexPattern: indexPatternAccessor, + hidden: true, + isNamespaceAgnostic: true, + }, + }, + savedObjectValidations: {}, + }; + + const converted = convertLegacyTypes(uiExports, legacyConfig); + + expect(indexPatternAccessor).toHaveBeenCalledWith(legacyConfig); + expect(legacyConfig.get).toHaveBeenCalledWith('foo.bar'); + expect(converted.length).toEqual(1); + expect(converted[0].indexPattern).toEqual('myIndex'); + }); + + it('import migrations from the uiExports', () => { + const migrationsA = { + '1.0.0': jest.fn(), + '2.0.4': jest.fn(), + }; + const migrationsB = { + '1.5.3': jest.fn(), + }; + + const uiExports: SavedObjectsLegacyUiExports = { + savedObjectMappings: [ + { + pluginId: 'pluginA', + properties: { + typeA: { + properties: { + fieldA: { type: 'text' }, + }, + }, + }, + }, + { + pluginId: 'pluginB', + properties: { + typeB: { + properties: { + fieldC: { type: 'text' }, + }, + }, + }, + }, + ], + savedObjectMigrations: { + typeA: migrationsA, + typeB: migrationsB, + }, + savedObjectSchemas: {}, + savedObjectValidations: {}, + }; + + const converted = convertLegacyTypes(uiExports, legacyConfig); + expect(converted.length).toEqual(2); + expect(converted[0].migrations).toEqual(migrationsA); + expect(converted[1].migrations).toEqual(migrationsB); + }); + + it('merges everything when all are present', () => { + const uiExports: SavedObjectsLegacyUiExports = { + savedObjectMappings: [ + { + pluginId: 'pluginA', + properties: { + typeA: { + properties: { + fieldA: { type: 'text' }, + }, + }, + typeB: { + properties: { + fieldB: { type: 'text' }, + anotherFieldB: { type: 'boolean' }, + }, + }, + }, + }, + { + pluginId: 'pluginB', + properties: { + typeC: { + properties: { + fieldC: { type: 'text' }, + }, + }, + }, + }, + ], + savedObjectMigrations: { + typeA: { + '1.0.0': jest.fn(), + '2.0.4': jest.fn(), + }, + typeC: { + '1.5.3': jest.fn(), + }, + }, + savedObjectSchemas: { + typeA: { + indexPattern: jest.fn(config => { + config.get('foo.bar'); + return 'myIndex'; + }), + hidden: true, + isNamespaceAgnostic: true, + }, + typeB: { + convertToAliasScript: 'some alias script', + hidden: false, + }, + }, + savedObjectValidations: {}, + }; + + const converted = convertLegacyTypes(uiExports, legacyConfig); + expect(converted).toMatchSnapshot(); + }); +}); + +describe('convertTypesToLegacySchema', () => { + it('converts types to the legacy schema format', () => { + const types: SavedObjectsType[] = [ + { + name: 'typeA', + hidden: false, + namespaceAgnostic: true, + mappings: { properties: {} }, + convertToAliasScript: 'some script', + }, + { + name: 'typeB', + hidden: true, + namespaceAgnostic: false, + indexPattern: 'myIndex', + mappings: { properties: {} }, + }, + ]; + expect(convertTypesToLegacySchema(types)).toEqual({ + typeA: { + hidden: false, + isNamespaceAgnostic: true, + convertToAliasScript: 'some script', + }, + typeB: { + hidden: true, + isNamespaceAgnostic: false, + indexPattern: 'myIndex', + }, + }); + }); +}); diff --git a/src/core/server/saved_objects/utils.ts b/src/core/server/saved_objects/utils.ts new file mode 100644 index 0000000000000..5c4d0ccb84b25 --- /dev/null +++ b/src/core/server/saved_objects/utils.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. + */ + +import { LegacyConfig } from '../legacy'; +import { SavedObjectsType, SavedObjectsLegacyUiExports } from './types'; +import { SavedObjectsSchemaDefinition } from './schema'; + +/** + * Converts the legacy savedObjects mappings, schema, and migrations + * to actual {@link SavedObjectsType | saved object types} + */ +export const convertLegacyTypes = ( + { + savedObjectMappings = [], + savedObjectMigrations = {}, + savedObjectSchemas = {}, + }: SavedObjectsLegacyUiExports, + legacyConfig: LegacyConfig +): SavedObjectsType[] => { + return savedObjectMappings.reduce((types, { pluginId, properties }) => { + return [ + ...types, + ...Object.entries(properties).map(([type, mappings]) => { + const schema = savedObjectSchemas[type]; + const migrations = savedObjectMigrations[type]; + return { + name: type, + hidden: schema?.hidden ?? false, + namespaceAgnostic: schema?.isNamespaceAgnostic ?? false, + mappings, + indexPattern: + typeof schema?.indexPattern === 'function' + ? schema.indexPattern(legacyConfig) + : schema?.indexPattern, + convertToAliasScript: schema?.convertToAliasScript, + migrations: migrations ?? {}, + }; + }), + ]; + }, [] as SavedObjectsType[]); +}; + +/** + * Convert {@link SavedObjectsType | saved object types} to the legacy {@link SavedObjectsSchemaDefinition | schema} format + */ +export const convertTypesToLegacySchema = ( + types: SavedObjectsType[] +): SavedObjectsSchemaDefinition => { + return types.reduce((schema, type) => { + return { + ...schema, + [type.name]: { + isNamespaceAgnostic: type.namespaceAgnostic, + hidden: type.hidden, + indexPattern: type.indexPattern, + convertToAliasScript: type.convertToAliasScript, + }, + }; + }, {} as SavedObjectsSchemaDefinition); +}; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index fb27fcccc2abe..cf8fafd7ea7c4 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1102,6 +1102,8 @@ export interface LogRecord { [name: string]: any; }; // (undocumented) + pid: number; + // (undocumented) timestamp: Date; } @@ -1421,6 +1423,19 @@ export interface SavedObjectAttributes { // @public export type SavedObjectAttributeSingle = string | number | boolean | null | undefined | SavedObjectAttributes; +// Warning: (ae-forgotten-export) The symbol "SavedObjectUnsanitizedDoc" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "SavedObjectMigrationFn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "SavedObjectUnsanitizedDoc" +// +// @public +export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, log: SavedObjectsMigrationLogger) => SavedObjectUnsanitizedDoc; + +// @public +export interface SavedObjectMigrationMap { + // (undocumented) + [version: string]: SavedObjectMigrationFn; +} + // @public export interface SavedObjectReference { // (undocumented) @@ -1431,6 +1446,12 @@ export interface SavedObjectReference { type: string; } +// Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; + // @public (undocumented) export interface SavedObjectsBaseOptions { namespace?: string; @@ -1534,6 +1555,32 @@ export interface SavedObjectsClientWrapperOptions { request: KibanaRequest; } +// @public +export interface SavedObjectsComplexFieldMapping { + // (undocumented) + dynamic?: string; + // (undocumented) + properties: SavedObjectsMappingProperties; + // (undocumented) + type?: string; +} + +// @public +export interface SavedObjectsCoreFieldMapping { + // (undocumented) + enabled?: boolean; + // (undocumented) + fields?: { + [subfield: string]: { + type: string; + }; + }; + // (undocumented) + index?: boolean; + // (undocumented) + type: string; +} + // @public (undocumented) export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { id?: string; @@ -1629,6 +1676,9 @@ export interface SavedObjectsExportResultDetails { }>; } +// @public +export type SavedObjectsFieldMapping = SavedObjectsCoreFieldMapping | SavedObjectsComplexFieldMapping; + // @public (undocumented) export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { // (undocumented) @@ -1793,6 +1843,12 @@ export interface SavedObjectsLegacyService { types: string[]; } +// @public +export interface SavedObjectsMappingProperties { + // (undocumented) + [field: string]: SavedObjectsFieldMapping; +} + // @public (undocumented) export interface SavedObjectsMigrationLogger { // (undocumented) @@ -1800,6 +1856,8 @@ export interface SavedObjectsMigrationLogger { // (undocumented) info: (msg: string) => void; // (undocumented) + warn: (msg: string) => void; + // @deprecated (undocumented) warning: (msg: string) => void; } @@ -1809,8 +1867,6 @@ export interface SavedObjectsMigrationVersion { [pluginName: string]: string; } -// Warning: (ae-missing-release-tag) "RawDoc" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface SavedObjectsRawDoc { // (undocumented) @@ -1819,8 +1875,10 @@ export interface SavedObjectsRawDoc { _primary_term?: number; // (undocumented) _seq_no?: number; + // Warning: (ae-forgotten-export) The symbol "SavedObjectsRawDocSource" needs to be exported by the entry point index.d.ts + // // (undocumented) - _source: any; + _source: SavedObjectsRawDocSource; // (undocumented) _type?: string; } @@ -1834,7 +1892,7 @@ export class SavedObjectsRepository { // Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal - static createRepository(migrator: KibanaMigrator, schema: SavedObjectsSchema, config: LegacyConfig, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; + static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, callCluster: APICaller, extraTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; // (undocumented) @@ -1873,7 +1931,7 @@ export interface SavedObjectsResolveImportErrorsOptions { supportedTypes: string[]; } -// @internal (undocumented) +// @internal @deprecated (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); @@ -1887,14 +1945,16 @@ export class SavedObjectsSchema { isNamespaceAgnostic(type: string): boolean; } -// @internal (undocumented) +// @public export class SavedObjectsSerializer { - constructor(schema: SavedObjectsSchema); + // Warning: (ae-forgotten-export) The symbol "ISavedObjectTypeRegistry" needs to be exported by the entry point index.d.ts + // + // @internal + constructor(registry: ISavedObjectTypeRegistry); generateRawId(namespace: string | undefined, type: string, id?: string): string; - isRawSavedObject(rawDoc: SavedObjectsRawDoc): any; - // Warning: (ae-forgotten-export) The symbol "SanitizedSavedObjectDoc" needs to be exported by the entry point index.d.ts - rawToSavedObject(doc: SavedObjectsRawDoc): SanitizedSavedObjectDoc; - savedObjectToRaw(savedObj: SanitizedSavedObjectDoc): SavedObjectsRawDoc; + isRawSavedObject(rawDoc: SavedObjectsRawDoc): boolean; + rawToSavedObject(doc: SavedObjectsRawDoc): SavedObjectSanitizedDoc; + savedObjectToRaw(savedObj: SavedObjectSanitizedDoc): SavedObjectsRawDoc; } // @public @@ -1907,9 +1967,27 @@ export interface SavedObjectsServiceSetup { export interface SavedObjectsServiceStart { createInternalRepository: (extraTypes?: string[]) => ISavedObjectsRepository; createScopedRepository: (req: KibanaRequest, extraTypes?: string[]) => ISavedObjectsRepository; + createSerializer: () => SavedObjectsSerializer; getScopedClient: (req: KibanaRequest, options?: SavedObjectsClientProviderOptions) => SavedObjectsClientContract; } +// @public (undocumented) +export interface SavedObjectsType { + convertToAliasScript?: string; + hidden: boolean; + indexPattern?: string; + mappings: SavedObjectsTypeMappingDefinition; + migrations?: SavedObjectMigrationMap; + name: string; + namespaceAgnostic: boolean; +} + +// @public +export interface SavedObjectsTypeMappingDefinition { + // (undocumented) + properties: SavedObjectsMappingProperties; +} + // @public (undocumented) export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions { references?: SavedObjectReference[]; @@ -1925,6 +2003,16 @@ export interface SavedObjectsUpdateResponse = { + const configKeys: Record = { 'xpack.task_manager.index': '.kibana_task_manager', }; + const config = { get: (path: string) => configKeys[path] }; + + const savedObjectTypes = convertLegacyTypes(uiExports, config as LegacyConfig); + const typeRegistry = new SavedObjectTypeRegistry(); + savedObjectTypes.forEach(type => typeRegistry.registerType(type)); const logger = { trace: log.verbose.bind(log), @@ -117,7 +124,6 @@ export async function migrateKibanaIndex({ }; const migratorOptions = { - config: { get: (path: string) => config[path] } as any, savedObjectsConfig: { scrollDuration: '5m', batchSize: 100, @@ -129,9 +135,7 @@ export async function migrateKibanaIndex({ } as any, logger, kibanaVersion, - savedObjectSchemas: new SavedObjectsSchema(uiExports.savedObjectSchemas), - savedObjectMappings: uiExports.savedObjectMappings, - savedObjectMigrations: uiExports.savedObjectMigrations, + typeRegistry, savedObjectValidations: uiExports.savedObjectValidations, callCluster: (path: string, ...args: any[]) => (get(client, path) as Function).call(client, ...args), diff --git a/src/legacy/core_plugins/console_legacy/public/styles/_app.scss b/src/legacy/core_plugins/console_legacy/public/styles/_app.scss index 3b6297f9cdbff..865a4fc7fafb0 100644 --- a/src/legacy/core_plugins/console_legacy/public/styles/_app.scss +++ b/src/legacy/core_plugins/console_legacy/public/styles/_app.scss @@ -48,6 +48,7 @@ button { line-height: inherit; } + position: absolute; z-index: $euiZLevel1; top: 0; @@ -89,3 +90,12 @@ .conApp__settingsModal { min-width: 460px; } + +.conApp__requestProgressBarContainer { + position: relative; + z-index: $euiZLevel2; +} + +.conApp__tabsExtension { + border-bottom: $euiBorderThin; +} diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts index 4ea5c78a9fd2b..7e1135ca96f9e 100644 --- a/src/legacy/core_plugins/data/public/actions/select_range_action.ts +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { - IAction, + Action, createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; @@ -55,7 +55,7 @@ async function isCompatible(context: ActionContext) { export function selectRangeAction( filterManager: FilterManager, timeFilter: TimefilterContract -): IAction { +): Action { return createAction({ type: SELECT_RANGE_ACTION, id: SELECT_RANGE_ACTION, diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts index 2f622eb1eb669..1e474b8f9355c 100644 --- a/src/legacy/core_plugins/data/public/actions/value_click_action.ts +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../plugins/kibana_react/public'; import { - IAction, + Action, createAction, IncompatibleActionError, } from '../../../../../plugins/ui_actions/public'; @@ -58,7 +58,7 @@ async function isCompatible(context: ActionContext) { export function valueClickAction( filterManager: FilterManager, timeFilter: TimefilterContract -): IAction { +): Action { return createAction({ type: VALUE_CLICK_ACTION, id: VALUE_CLICK_ACTION, diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index ebc470555d87c..e13e8e34eaebe 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -43,19 +43,19 @@ import { VALUE_CLICK_TRIGGER, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/embeddable/public/lib/triggers'; -import { IUiActionsSetup, IUiActionsStart } from '../../../../plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from '../../../../plugins/ui_actions/public'; import { SearchSetup, SearchStart, SearchService } from './search/search_service'; export interface DataPluginSetupDependencies { data: DataPublicPluginSetup; expressions: ExpressionsSetup; - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; } export interface DataPluginStartDependencies { data: DataPublicPluginStart; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; } /** diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 14bdef99ca894..ff70ed9dc2eb7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -30,11 +30,9 @@ export const legacyChrome = chrome; export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { IPrivate } from 'ui/private'; -export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore export { ConfirmationButtonTypes } from 'ui/modals/confirm_modal'; -export { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_save_modal'; export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; export { KbnUrl } from 'ui/url/kbn_url'; // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts index 6404b5bdbd63b..86d399d219a26 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts @@ -28,6 +28,7 @@ import { const mockLogger = { warning: () => {}, + warn: () => {}, debug: () => {}, info: () => {}, }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 624be02ac3b9d..9f6b01d5beb49 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -31,8 +31,6 @@ import { ConfirmationButtonTypes, migrateLegacyQuery, SavedObjectSaveOpts, - SaveResult, - showSaveModal, subscribeWithScope, } from '../legacy_imports'; import { @@ -45,6 +43,11 @@ import { syncAppFilters, syncQuery, } from '../../../../../../plugins/data/public'; +import { + SaveResult, + showSaveModal, + getSavedObjectFinder, +} from '../../../../../../plugins/saved_objects/public'; import { DASHBOARD_CONTAINER_TYPE, @@ -74,10 +77,6 @@ import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; -import { - SavedObjectFinderProps, - SavedObjectFinderUi, -} from '../../../../../../plugins/kibana_react/public'; import { IKbnUrlStateStorage, removeQueryParam, @@ -114,7 +113,16 @@ export class DashboardAppController { share, dashboardCapabilities, npDataStart: { query: queryService }, - core: { notifications, overlays, chrome, injectedMetadata, uiSettings, savedObjects, http }, + core: { + notifications, + overlays, + chrome, + injectedMetadata, + uiSettings, + savedObjects, + http, + i18n: i18nStart, + }, history, kbnUrlStateStorage, }: DashboardAppControllerDependencies) { @@ -777,7 +785,7 @@ export class DashboardAppController { showCopyOnSave={dash.id ? true : false} /> ); - showSaveModal(dashboardSaveModal); + showSaveModal(dashboardSaveModal, i18nStart.Context); }; navActions[TopNavIds.CLONE] = () => { const currentTitle = dashboardStateManager.getTitle(); @@ -806,17 +814,13 @@ export class DashboardAppController { }; navActions[TopNavIds.ADD] = () => { if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { - const SavedObjectFinder = (props: SavedObjectFinderProps) => ( - - ); - openAddPanelFlyout({ embeddable: dashboardContainer, getAllFactories: embeddables.getEmbeddableFactories, getFactory: embeddables.getEmbeddableFactory, notifications, overlays, - SavedObjectFinder, + SavedObjectFinder: getSavedObjectFinder(savedObjects, uiSettings), }); } }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.test.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.test.js index 5f708a22fd530..e3933ce9d0143 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.test.js @@ -20,7 +20,7 @@ import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -jest.mock('../../legacy_imports', () => ({ +jest.mock('../../../../../../../plugins/saved_objects/public', () => ({ SavedObjectSaveModal: () => null, })); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx index bd53fd5a13083..026784fcae06f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/save_modal.tsx @@ -21,7 +21,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui'; -import { SavedObjectSaveModal } from '../../legacy_imports'; +import { SavedObjectSaveModal } from '../../../../../../../plugins/saved_objects/public'; interface SaveOptions { newTitle: string; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 016bfac751875..7ae1c723a3914 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -67,7 +67,7 @@ export interface DashboardPluginSetupDependencies { getAngularDependencies: () => Promise; }; home: HomePublicPluginSetup; - kibana_legacy: KibanaLegacySetup; + kibanaLegacy: KibanaLegacySetup; npData: NpDataSetup; } @@ -88,7 +88,7 @@ export class DashboardPlugin implements Plugin { { __LEGACY: { getAngularDependencies }, home, - kibana_legacy, + kibanaLegacy, npData, }: DashboardPluginSetupDependencies ) { @@ -146,7 +146,7 @@ export class DashboardPlugin implements Plugin { chrome: contextCore.chrome, addBasePath: contextCore.http.basePath.prepend, uiSettings: contextCore.uiSettings, - config: kibana_legacy.config, + config: kibanaLegacy.config, savedQueryService: npDataStart.query.savedQueries, embeddables, dashboardCapabilities: contextCore.application.capabilities.dashboard, @@ -160,14 +160,14 @@ export class DashboardPlugin implements Plugin { }; }, }; - kibana_legacy.registerLegacyApp({ + kibanaLegacy.registerLegacyApp({ ...app, id: 'dashboard', // only register the updater in once app, otherwise all updates would happen twice updater$: this.appStateUpdater.asObservable(), navLinkId: 'kibana:dashboard', }); - kibana_legacy.registerLegacyApp({ ...app, id: 'dashboards' }); + kibanaLegacy.registerLegacyApp({ ...app, id: 'dashboards' }); home.featureCatalogue.register({ id: 'dashboard', diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/index.ts b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts index f2555259028cc..6515674c6b52a 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/index.ts +++ b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts @@ -22,7 +22,7 @@ import 'uiExports/devTools'; import { npStart } from 'ui/new_platform'; -if (npStart.plugins.dev_tools.getSortedDevTools().length === 0) { +if (npStart.plugins.devTools.getSortedDevTools().length === 0) { npStart.core.chrome.navLinks.update('kibana:dev_tools', { hidden: true, }); diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index f698a2ee361e0..820c9949342a4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -58,8 +58,6 @@ export { getRequestInspectorStats, getResponseInspectorStats } from '../../../da export { intervalOptions } from 'ui/agg_types'; // @ts-ignore export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; -export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; export { stateMonitorFactory } from 'ui/state_management/state_monitor_factory'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.html index 3e0f8a8329154..8bbb746fa45f8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.html @@ -2,12 +2,11 @@ app-name="'context'" show-search-bar="true" show-filter-bar="true" + show-query-bar="false" show-save-query="false" show-date-picker="false" - - filters="contextApp.state.queryParameters.filters" - on-filters-updated="contextApp.actions.updateFilters" index-patterns="[contextApp.indexPattern]" + use-default-behaviors="true" > diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js index 5fa0958249d79..6549f13556373 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_app.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { getServices, callAfterBindingsWorkaround, getAngularModule } from '../../kibana_services'; +import { callAfterBindingsWorkaround, getAngularModule } from '../../kibana_services'; import contextAppTemplate from './context_app.html'; import './context/components/action_bar'; import { getFirstSortableField } from './context/api/utils/sorting'; @@ -34,8 +34,6 @@ import { QueryActionsProvider, } from './context/query'; -const { timefilter } = getServices(); - const module = getAngularModule(); module.directive('contextApp', function ContextApp() { @@ -61,10 +59,6 @@ module.directive('contextApp', function ContextApp() { function ContextAppController($scope, config, Private) { const queryParameterActions = getQueryParameterActions(); const queryActions = Private(QueryActionsProvider); - - timefilter.disableAutoRefreshSelector(); - timefilter.disableTimeRangeSelector(); - this.state = createInitialState( parseInt(config.get('context:step'), 10), getFirstSortableField(this.indexPattern, config.get('context:tieBreakerFields')), diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx index 4d387f44c3d57..77bbab97d95c7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx @@ -30,7 +30,6 @@ import { Axis, Chart, HistogramBarSeries, - GeometryValue, LineAnnotation, Position, ScaleType, @@ -38,6 +37,7 @@ import { RectAnnotation, TooltipValue, TooltipType, + ElementClickListener, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; @@ -139,7 +139,7 @@ export class DiscoverHistogram extends Component (elementData: GeometryValue[]) => { + public onElementClick = (xInterval: number): ElementClickListener => ([elementData]) => { const startRange = elementData[0].x; const range = { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index 45490ac7adc0f..efde83a0e35f0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -5,24 +5,19 @@

{{screenTitle}}

diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 2f73af2ab77e4..69f69d449354c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -19,13 +19,18 @@ import _ from 'lodash'; import React from 'react'; -import { Subscription } from 'rxjs'; +import { Subscription, Subject, merge } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import { i18n } from '@kbn/i18n'; import '../components/field_chooser/field_chooser'; import { RequestAdapter } from '../../../../../../../plugins/inspector/public'; +import { + SavedObjectSaveModal, + showSaveModal, +} from '../../../../../../../plugins/saved_objects/public'; // doc table import './doc_table'; import { getSortArray } from './doc_table/lib/get_sort'; @@ -47,12 +52,10 @@ import { hasSearchStategyForIndexPattern, intervalOptions, migrateLegacyQuery, - showSaveModal, unhashUrl, stateMonitorFactory, subscribeWithScope, tabifyAggResponse, - SavedObjectSaveModal, getAngularModule, ensureDefaultIndexPattern, registerTimefilterWithGlobalStateFactory, @@ -62,7 +65,6 @@ import { Vis } from '../../../../../visualizations/public'; const { core, chrome, - data, docTitle, filterManager, share, @@ -79,8 +81,6 @@ import { import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { FilterStateManager } from '../../../../../data/public'; -const { getSavedQuery } = data.query.savedQueries; - const fetchStatuses = { UNINITIALIZED: 'uninitialized', LOADING: 'loading', @@ -205,8 +205,6 @@ function discoverController( const subscriptions = new Subscription(); - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); $scope.timefilterUpdateHandler = ranges => { timefilter.setTime({ from: moment(ranges.from).toISOString(), @@ -218,7 +216,6 @@ function discoverController( $scope.showInterval = false; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; - $scope.refreshInterval = timefilter.getRefreshInterval(); $scope.showSaveQuery = uiCapabilities.discover.saveQuery; $scope.$watch( @@ -310,7 +307,7 @@ function discoverController( })} /> ); - showSaveModal(saveModal); + showSaveModal(saveModal, core.i18n.Context); }, }; @@ -436,15 +433,10 @@ function discoverController( let stateMonitor; const $state = ($scope.state = new AppState(getStateDefaults())); + const $fetchObservable = new Subject(); - $scope.filters = filterManager.getFilters(); $scope.screenTitle = savedSearch.title; - $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the filterManager emits an update event (see below) - filterManager.setFilters(filters); - }; - const getFieldCounts = async () => { // the field counts aren't set until we have the data back, // so we wait for the fetch to be done before proceeding @@ -571,17 +563,12 @@ function discoverController( }; const shouldSearchOnPageLoad = () => { - // If a saved query is referenced in the app state, omit the initial load because the saved query will - // be fetched separately and trigger a reload - if ($scope.state.savedQuery) { - return false; - } // A saved search is created on every page load, so we check the ID to see if we're loading a // previously saved search or if it is just transient return ( config.get('discover:searchOnPageLoad') || savedSearch.id !== undefined || - _.get($scope, 'refreshInterval.pause') === false + timefilter.getRefreshInterval().pause === false ); }; @@ -593,25 +580,23 @@ function discoverController( $scope.$on('$destroy', () => stateMonitor.destroy()); $scope.updateDataSource().then(function() { - subscriptions.add( - subscribeWithScope($scope, timefilter.getAutoRefreshFetch$(), { - next: $scope.fetch, - }) - ); + const searchBarChanges = merge( + timefilter.getAutoRefreshFetch$(), + timefilter.getFetch$(), + filterManager.getFetches$(), + $fetchObservable + ).pipe(debounceTime(100)); subscriptions.add( - subscribeWithScope($scope, timefilter.getRefreshIntervalUpdate$(), { - next: $scope.updateRefreshInterval, + subscribeWithScope($scope, searchBarChanges, { + next: $scope.fetch, }) ); subscriptions.add( subscribeWithScope($scope, timefilter.getTimeUpdate$(), { - next: $scope.updateTime, - }) - ); - subscriptions.add( - subscribeWithScope($scope, timefilter.getFetch$(), { - next: $scope.fetch, + next: () => { + $scope.updateTime(); + }, }) ); @@ -622,14 +607,14 @@ function discoverController( const currentSort = getSortArray($scope.searchSource.getField('sort'), $scope.indexPattern); // if the searchSource doesn't know, tell it so - if (!angular.equals(sort, currentSort)) $scope.fetch(); + if (!angular.equals(sort, currentSort)) $fetchObservable.next(); }); // update data source when filters update + subscriptions.add( subscribeWithScope($scope, filterManager.getUpdates$(), { next: () => { - $scope.filters = filterManager.getFilters(); $scope.updateDataSource().then(function() { $state.save(); }); @@ -637,16 +622,9 @@ function discoverController( }) ); - // fetch data when filters fire fetch event - subscriptions.add( - subscribeWithScope($scope, filterManager.getFetches$(), { - next: $scope.fetch, - }) - ); - // update data source when hitting forward/back and the query changes $scope.$listen($state, 'fetch_with_changes', function(diff) { - if (diff.indexOf('query') >= 0) $scope.fetch(); + if (diff.indexOf('query') >= 0) $fetchObservable.next(); }); $scope.$watch('opts.timefield', function(timefield) { @@ -655,7 +633,7 @@ function discoverController( $scope.$watch('state.interval', function(newInterval, oldInterval) { if (newInterval !== oldInterval) { - $scope.fetch(); + $fetchObservable.next(); } }); @@ -674,7 +652,7 @@ function discoverController( if (!_.isEqual(newQuery, oldQuery)) { const query = migrateLegacyQuery(newQuery); if (!_.isEqual(query, newQuery)) { - $scope.updateQueryAndFetch({ query }); + $scope.updateQuery({ query }); } } }); @@ -734,7 +712,7 @@ function discoverController( $state.replace(); if (shouldSearchOnPageLoad()) { - $scope.fetch(); + $fetchObservable.next(); } }); }); @@ -827,15 +805,9 @@ function discoverController( }); }; - $scope.updateQueryAndFetch = function({ query, dateRange }) { - const oldDateRange = timefilter.getTime(); - timefilter.setTime(dateRange); + $scope.updateQuery = function({ query }) { $state.query = query; - // storing the updated timerange in the state will trigger a fetch - // call automatically, so only trigger fetch in case this is a refresh call (no changes in parameters). - if (_.isEqual(oldDateRange, dateRange)) { - $scope.fetch(); - } + $fetchObservable.next(); }; function onResults(resp) { @@ -896,32 +868,12 @@ function discoverController( from: dateMath.parse(timefilter.getTime().from), to: dateMath.parse(timefilter.getTime().to, { roundUp: true }), }; - $scope.time = timefilter.getTime(); }; $scope.toMoment = function(datetime) { return moment(datetime).format(config.get('dateFormat')); }; - $scope.updateRefreshInterval = function() { - const newInterval = timefilter.getRefreshInterval(); - const shouldFetch = - _.get($scope, 'refreshInterval.pause') === true && newInterval.pause === false; - - $scope.refreshInterval = newInterval; - - if (shouldFetch) { - $scope.fetch(); - } - }; - - $scope.onRefreshChange = function({ isPaused, refreshInterval }) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : $scope.refreshInterval.value, - }); - }; - $scope.resetQuery = function() { kbnUrl.change('/discover/{{id}}', { id: $route.current.params.id }); }; @@ -988,69 +940,14 @@ function discoverController( $scope.minimumVisibleRows = $scope.hits; }; - $scope.onQuerySaved = savedQuery => { - $scope.savedQuery = savedQuery; - }; - - $scope.onSavedQueryUpdated = savedQuery => { - $scope.savedQuery = { ...savedQuery }; - }; - - $scope.onClearSavedQuery = () => { - delete $scope.savedQuery; - delete $state.savedQuery; - $state.query = { - query: '', - language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), - }; - filterManager.setFilters(filterManager.getGlobalFilters()); - $state.save(); - $scope.fetch(); - }; - - const updateStateFromSavedQuery = savedQuery => { - $state.query = savedQuery.attributes.query; - $state.save(); - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = filterManager.getGlobalFilters(); - filterManager.setFilters([...globalFilters, ...savedQueryFilters]); - - if (savedQuery.attributes.timefilter) { - timefilter.setTime({ - from: savedQuery.attributes.timefilter.from, - to: savedQuery.attributes.timefilter.to, - }); - if (savedQuery.attributes.timefilter.refreshInterval) { - timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval); - } + $scope.updateSavedQueryId = newSavedQueryId => { + if (newSavedQueryId) { + $state.savedQuery = newSavedQueryId; + } else { + delete $state.savedQuery; } - - $scope.fetch(); - }; - - $scope.$watch('savedQuery', newSavedQuery => { - if (!newSavedQuery) return; - - $state.savedQuery = newSavedQuery.id; $state.save(); - - updateStateFromSavedQuery(newSavedQuery); - }); - - $scope.$watch('state.savedQuery', newSavedQueryId => { - if (!newSavedQueryId) { - $scope.savedQuery = undefined; - return; - } - if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { - getSavedQuery(newSavedQueryId).then(savedQuery => { - $scope.$evalAsync(() => { - $scope.savedQuery = savedQuery; - updateStateFromSavedQuery(savedQuery); - }); - }); - } - }); + }; async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/use_es_doc_search.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/use_es_doc_search.ts index d3bf3696c08a5..6cffc2cc533b0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/use_es_doc_search.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/doc/use_es_doc_search.ts @@ -62,36 +62,34 @@ export function useEsDocSearch({ const [status, setStatus] = useState(ElasticRequestState.Loading); const [hit, setHit] = useState(null); - async function requestData() { - try { - const indexPatternEntity = await indexPatternService.get(indexPatternId); - setIndexPattern(indexPatternEntity); + useEffect(() => { + async function requestData() { + try { + const indexPatternEntity = await indexPatternService.get(indexPatternId); + setIndexPattern(indexPatternEntity); - const { hits } = await esClient.search({ - index, - body: buildSearchBody(id, indexPatternEntity), - }); + const { hits } = await esClient.search({ + index, + body: buildSearchBody(id, indexPatternEntity), + }); - if (hits && hits.hits && hits.hits[0]) { - setStatus(ElasticRequestState.Found); - setHit(hits.hits[0]); - } else { - setStatus(ElasticRequestState.NotFound); - } - } catch (err) { - if (err.savedObjectId) { - setStatus(ElasticRequestState.NotFoundIndexPattern); - } else if (err.status === 404) { - setStatus(ElasticRequestState.NotFound); - } else { - setStatus(ElasticRequestState.Error); + if (hits && hits.hits && hits.hits[0]) { + setStatus(ElasticRequestState.Found); + setHit(hits.hits[0]); + } else { + setStatus(ElasticRequestState.NotFound); + } + } catch (err) { + if (err.savedObjectId) { + setStatus(ElasticRequestState.NotFoundIndexPattern); + } else if (err.status === 404) { + setStatus(ElasticRequestState.NotFound); + } else { + setStatus(ElasticRequestState.Error); + } } } - } - - useEffect(() => { requestData(); - }); - + }, [esClient, id, index, indexPatternId, indexPatternService]); return [status, hit, indexPattern]; } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/top_nav/open_search_panel.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/top_nav/open_search_panel.js index ebe4cbb1ddb69..747dd9abe2f2a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/top_nav/open_search_panel.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/top_nav/open_search_panel.js @@ -32,7 +32,7 @@ import { EuiFlyoutBody, EuiTitle, } from '@elastic/eui'; -import { SavedObjectFinderUi } from '../../../../../../../../plugins/kibana_react/public'; +import { SavedObjectFinderUi } from '../../../../../../../../plugins/saved_objects/public'; import { getServices } from '../../../kibana_services'; const SEARCH_OBJECT_TYPE = 'search'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts index f47cf52c756ac..3f877520b5bf9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import * as Rx from 'rxjs'; import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { RequestAdapter, Adapters } from '../../../../../../../plugins/inspector/public'; import { esFilters, @@ -111,7 +111,7 @@ export class SearchEmbeddable extends Embeddable filterManager, }: SearchEmbeddableConfig, initialInput: SearchInput, - private readonly executeTriggerActions: TExecuteTriggerActions, + private readonly executeTriggerActions: ExecuteTriggerActions, parent?: Container ) { super( diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts index 842ef2bf9c825..15b3f2d4517ac 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts @@ -19,7 +19,7 @@ import { auto } from 'angular'; import { i18n } from '@kbn/i18n'; -import { TExecuteTriggerActions } from 'src/plugins/ui_actions/public'; +import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { getServices } from '../../kibana_services'; import { EmbeddableFactory, @@ -43,7 +43,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< public isEditable: () => boolean; constructor( - private readonly executeTriggerActions: TExecuteTriggerActions, + private readonly executeTriggerActions: ExecuteTriggerActions, getInjector: () => Promise, isEditable: () => boolean ) { diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 77135f6a98173..a495b56d5e9ea 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import angular, { auto } from 'angular'; -import { IUiActionsSetup, IUiActionsStart } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; @@ -47,13 +47,13 @@ export interface DiscoverSetup { } export type DiscoverStart = void; export interface DiscoverSetupPlugins { - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; embeddable: IEmbeddableSetup; - kibana_legacy: KibanaLegacySetup; + kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; } export interface DiscoverStartPlugins { - uiActions: IUiActionsStart; + uiActions: UiActionsStart; embeddable: IEmbeddableStart; navigation: NavigationStart; charts: ChartsPluginStart; @@ -99,7 +99,7 @@ export class DiscoverPlugin implements Plugin { order: 20, component: JsonCodeBlock, }); - plugins.kibana_legacy.registerLegacyApp({ + plugins.kibanaLegacy.registerLegacyApp({ id: 'discover', title: 'Discover', order: -1004, diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index aec3835dc075d..5802f33627fb3 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -59,7 +59,7 @@ export interface HomePluginSetupDependencies { getAngularDependencies: () => Promise; }; usageCollection: UsageCollectionSetup; - kibana_legacy: KibanaLegacySetup; + kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; } @@ -72,12 +72,12 @@ export class HomePlugin implements Plugin { core: CoreSetup, { home, - kibana_legacy, + kibanaLegacy, usageCollection, __LEGACY: { getAngularDependencies, ...legacyServices }, }: HomePluginSetupDependencies ) { - kibana_legacy.registerLegacyApp({ + kibanaLegacy.registerLegacyApp({ id: 'home', title: 'Home', mount: async ({ core: contextCore }, params) => { @@ -98,7 +98,7 @@ export class HomePlugin implements Plugin { getBasePath: core.http.basePath.get, indexPatternService: this.dataStart!.indexPatterns, environment: this.environment!, - config: kibana_legacy.config, + config: kibanaLegacy.config, homeConfig: home.config, ...angularDependencies, }); diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index f2868da947a75..d77fc780f4cc2 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -57,13 +57,13 @@ import { showAppRedirectNotification } from 'ui/notify'; import 'leaflet'; import { localApplicationService } from './local_application_service'; -npSetup.plugins.kibana_legacy.forwardApp('doc', 'discover', { keepPrefix: true }); -npSetup.plugins.kibana_legacy.forwardApp('context', 'discover', { keepPrefix: true }); +npSetup.plugins.kibanaLegacy.forwardApp('doc', 'discover', { keepPrefix: true }); +npSetup.plugins.kibanaLegacy.forwardApp('context', 'discover', { keepPrefix: true }); localApplicationService.attachToAngular(routes); routes.enable(); -const { config } = npSetup.plugins.kibana_legacy; +const { config } = npSetup.plugins.kibanaLegacy; routes.otherwise({ redirectTo: `/${config.defaultAppId || 'discover'}`, }); diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index c84a3e1eacbd2..90328003c8292 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -50,7 +50,7 @@ export class LocalApplicationService { * @param angularRouteManager The current `ui/routes` instance */ attachToAngular(angularRouteManager: UIRoutes) { - npStart.plugins.kibana_legacy.getApps().forEach(app => { + npStart.plugins.kibanaLegacy.getApps().forEach(app => { const wrapperElementId = this.idGenerator(); angularRouteManager.when(matchAllWithPrefix(app), { outerAngularWrapperRoute: true, @@ -92,7 +92,7 @@ export class LocalApplicationService { } }); - npStart.plugins.kibana_legacy.getForwards().forEach(({ legacyAppId, newAppId, keepPrefix }) => { + npStart.plugins.kibanaLegacy.getForwards().forEach(({ legacyAppId, newAppId, keepPrefix }) => { angularRouteManager.when(matchAllWithPrefix(legacyAppId), { resolveRedirectTo: ($location: ILocationService) => { const url = $location.url(); diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts index e7170836cf749..83b820a8e3134 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/index.ts @@ -21,7 +21,6 @@ import { PluginInitializerContext } from 'kibana/public'; import { VisualizePlugin } from './plugin'; export * from './np_ready/visualize_constants'; -export { showNewVisModal } from './np_ready/wizard'; export { VisualizeConstants, createVisualizeEditUrl } from './np_ready/visualize_constants'; // Core will be looking for this when loading our plugin in the new platform diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 15e9c73a39eff..428e6cb225710 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -23,6 +23,7 @@ import { SavedObjectsClientContract, ToastsStart, IUiSettingsClient, + I18nStart, } from 'kibana/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -57,6 +58,7 @@ export interface VisualizeKibanaServices { visualizeCapabilities: any; visualizations: VisualizationsStart; usageCollection?: UsageCollectionSetup; + I18nContext: I18nStart['Context']; } let services: VisualizeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index b185dc577a3aa..bb24870c5a5f3 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -44,9 +44,6 @@ export { IPrivate } from 'ui/private'; // @ts-ignore export { PrivateProvider } from 'ui/private/private'; -export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; -export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; - export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss b/src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss index f97ae012055b0..847e72bf29fbc 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/_index.scss @@ -7,4 +7,3 @@ @import 'editor/index'; @import 'listing/index'; -@import 'wizard/index'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 96a583bec7dc9..46ae45c3a5fa2 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -31,6 +31,10 @@ import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { FilterStateManager } from '../../../../../data/public'; import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; +import { + SavedObjectSaveModal, + showSaveModal, +} from '../../../../../../../plugins/saved_objects/public'; import { initVisEditorDirective } from './visualization_editor'; import { initVisualizationDirective } from './visualization'; @@ -40,8 +44,6 @@ import { absoluteToParsedUrl, KibanaParsedUrl, migrateLegacyQuery, - SavedObjectSaveModal, - showSaveModal, stateMonitorFactory, DashboardConstants, } from '../../legacy_imports'; @@ -94,10 +96,10 @@ function VisualizeAppController( core: { docLinks }, savedQueryService, uiSettings, + I18nContext, } = getServices(); const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); - const queryFilter = filterManager; // Retrieve the resolved SavedVis instance. const savedVis = $route.current.locals.savedVis; const _applyVis = () => { @@ -192,7 +194,7 @@ function VisualizeAppController( description={savedVis.description} /> ); - showSaveModal(saveModal); + showSaveModal(saveModal, I18nContext); }, }, ] @@ -311,11 +313,11 @@ function VisualizeAppController( return appState; })(); - $scope.filters = queryFilter.getFilters(); + $scope.filters = filterManager.getFilters(); $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); + // The filters will automatically be set when the filterManager emits an update event (see below) + filterManager.setFilters(filters); }; $scope.showSaveQuery = visualizeCapabilities.saveQuery; @@ -426,15 +428,15 @@ function VisualizeAppController( // update the searchSource when filters update subscriptions.add( - subscribeWithScope($scope, queryFilter.getUpdates$(), { + subscribeWithScope($scope, filterManager.getUpdates$(), { next: () => { - $scope.filters = queryFilter.getFilters(); - $scope.globalFilters = queryFilter.getGlobalFilters(); + $scope.filters = filterManager.getFilters(); + $scope.globalFilters = filterManager.getGlobalFilters(); }, }) ); subscriptions.add( - subscribeWithScope($scope, queryFilter.getFetches$(), { + subscribeWithScope($scope, filterManager.getFetches$(), { next: $scope.fetch, }) ); @@ -500,7 +502,7 @@ function VisualizeAppController( language: localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), }; - queryFilter.setFilters(queryFilter.getGlobalFilters()); + filterManager.setFilters(filterManager.getGlobalFilters()); $state.save(); $scope.fetch(); }; @@ -510,8 +512,8 @@ function VisualizeAppController( $state.save(); const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = queryFilter.getGlobalFilters(); - queryFilter.setFilters([...globalFilters, ...savedQueryFilters]); + const globalFilters = filterManager.getGlobalFilters(); + filterManager.setFilters([...globalFilters, ...savedQueryFilters]); if (savedQuery.attributes.timefilter) { timefilter.setTime({ diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html index 522d20fffafd3..17caaba144a44 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.html @@ -9,15 +9,4 @@ edit-item="listingController.editItem" listing-limit="listingController.listingLimit" > - - - diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js index ac8308e1cd1c5..6bac4e4c62efa 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js @@ -19,7 +19,7 @@ import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { VisualizeListingTable } from './visualize_listing_table'; -import { NewVisModal } from '../wizard/new_vis_modal'; + import { VisualizeConstants } from '../visualize_constants'; import { i18n } from '@kbn/i18n'; @@ -30,17 +30,6 @@ export function initListingDirective(app) { app.directive('visualizeListingTable', reactDirective => reactDirective(wrapInI18nContext(VisualizeListingTable)) ); - app.directive('newVisModal', reactDirective => - reactDirective(wrapInI18nContext(NewVisModal), [ - ['visTypesRegistry', { watchDepth: 'collection' }], - ['onClose', { watchDepth: 'reference' }], - ['addBasePath', { watchDepth: 'reference' }], - ['uiSettings', { watchDepth: 'reference' }], - ['savedObjects', { watchDepth: 'reference' }], - ['usageCollection', { watchDepth: 'reference' }], - 'isOpen', - ]) - ); } export function VisualizeListingController($injector, createNewVis) { @@ -59,21 +48,18 @@ export function VisualizeListingController($injector, createNewVis) { uiSettings, visualizations, core: { docLinks, savedObjects }, - usageCollection, } = getServices(); const kbnUrl = $injector.get('kbnUrl'); timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); - this.showNewVisModal = false; this.addBasePath = addBasePath; this.uiSettings = uiSettings; this.savedObjects = savedObjects; - this.usageCollection = usageCollection; this.createNewVis = () => { - this.showNewVisModal = true; + visualizations.showNewVisModal(); }; this.editItem = ({ editUrl }) => { @@ -85,19 +71,15 @@ export function VisualizeListingController($injector, createNewVis) { return addBasePath(editUrl); }; - this.closeNewVisModal = () => { - this.showNewVisModal = false; - // In case the user came via a URL to this page, change the URL to the regular landing page URL after closing the modal - if (createNewVis) { - kbnUrl.changePath(VisualizeConstants.LANDING_PAGE_PATH); - } - }; - if (createNewVis) { // In case the user navigated to the page via the /visualize/new URL we start the dialog immediately - this.createNewVis(); + visualizations.showNewVisModal({ + onClose: () => { + // In case the user came via a URL to this page, change the URL to the regular landing page URL after closing the modal + kbnUrl.changePath(VisualizeConstants.LANDING_PAGE_PATH); + }, + }); } - this.visTypeRegistry = visualizations.types; this.fetchItems = filter => { const isLabsEnabled = uiSettings.get('visualize:enableLabs'); diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 8e7487fee55f6..ce93fe7c2d578 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -56,7 +56,7 @@ export interface VisualizePluginSetupDependencies { legacyChrome: Chrome; }; home: HomePublicPluginSetup; - kibana_legacy: KibanaLegacySetup; + kibanaLegacy: KibanaLegacySetup; usageCollection?: UsageCollectionSetup; } @@ -72,9 +72,9 @@ export class VisualizePlugin implements Plugin { public async setup( core: CoreSetup, - { home, kibana_legacy, __LEGACY, usageCollection }: VisualizePluginSetupDependencies + { home, kibanaLegacy, __LEGACY, usageCollection }: VisualizePluginSetupDependencies ) { - kibana_legacy.registerLegacyApp({ + kibanaLegacy.registerLegacyApp({ id: 'visualize', title: 'Visualize', mount: async ({ core: contextCore }, params) => { @@ -108,10 +108,11 @@ export class VisualizePlugin implements Plugin { share, toastNotifications: contextCore.notifications.toasts, uiSettings: contextCore.uiSettings, - config: kibana_legacy.config, + config: kibanaLegacy.config, visualizeCapabilities: contextCore.application.capabilities.visualize, visualizations, usageCollection, + I18nContext: contextCore.i18n.Context, }; setServices(deps); diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index e5836c1372068..03471174753fa 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -20,31 +20,24 @@ import { i18n } from '@kbn/i18n'; import { SavedObjectAttributes } from 'kibana/public'; import { - EmbeddableFactory, - ErrorEmbeddable, Container, + EmbeddableFactory, EmbeddableOutput, + ErrorEmbeddable, } from '../../../../../plugins/embeddable/public'; -import { showNewVisModal } from '../../../kibana/public/visualize'; import { SavedVisualizations } from '../../../kibana/public/visualize/np_ready/types'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { getIndexPattern } from './get_index_pattern'; import { + VisSavedObject, VisualizeEmbeddable, VisualizeInput, VisualizeOutput, - VisSavedObject, } from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -import { - getUISettings, - getCapabilities, - getHttp, - getTypes, - getSavedObjects, - getUsageCollector, -} from '../np_ready/public/services'; +import { getCapabilities, getHttp, getTypes, getUISettings } from '../np_ready/public/services'; +import { showNewVisModal } from '../np_ready/public/wizard'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; @@ -157,16 +150,9 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< public async create() { // TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up // to allow for in place creation of visualizations without having to navigate away to a new URL. - showNewVisModal( - getTypes(), - { - editorParams: ['addToDashboard'], - }, - getHttp().basePath.prepend, - getUISettings(), - getSavedObjects(), - getUsageCollector() - ); + showNewVisModal({ + editorParams: ['addToDashboard'], + }); return undefined; } } diff --git a/src/legacy/core_plugins/visualizations/public/index.scss b/src/legacy/core_plugins/visualizations/public/index.scss index 957d06be4daf0..748945eabd331 100644 --- a/src/legacy/core_plugins/visualizations/public/index.scss +++ b/src/legacy/core_plugins/visualizations/public/index.scss @@ -1,3 +1,4 @@ @import 'src/legacy/ui/public/styles/styling_constants'; -@import './embeddable/_index'; +@import './embeddable/index'; +@import './np_ready/public/index'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss new file mode 100644 index 0000000000000..d87b6b004a511 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/_index.scss @@ -0,0 +1 @@ +@import 'wizard/index'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index fc85970b906ae..1063b1718b851 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -48,6 +48,7 @@ const createStartContract = (): VisualizationsStart => ({ getAliases: jest.fn(), }, getSavedVisualizationsLoader: jest.fn(), + showNewVisModal: jest.fn(), }); const createInstance = async () => { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 01059044b98c2..cbbeb7ff980a6 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -43,6 +43,7 @@ import { SavedObjectKibanaServicesWithVisualizations, } from '../../saved_visualizations'; import { SavedVisualizations } from '../../../../kibana/public/visualize/np_ready/types'; +import { showNewVisModal } from './wizard'; /** * Interface for this plugin's returned setup/start contracts. * @@ -55,6 +56,7 @@ export interface VisualizationsSetup { export interface VisualizationsStart { types: TypesStart; getSavedVisualizationsLoader: () => SavedVisualizations; + showNewVisModal: typeof showNewVisModal; } export interface VisualizationsSetupDeps { @@ -92,7 +94,7 @@ export class VisualizationsPlugin public setup( core: CoreSetup, { expressions, embeddable, usageCollection }: VisualizationsSetupDeps - ) { + ): VisualizationsSetup { setUISettings(core.uiSettings); setUsageCollector(usageCollection); @@ -107,7 +109,7 @@ export class VisualizationsPlugin }; } - public start(core: CoreStart, { data }: VisualizationsStartDeps) { + public start(core: CoreStart, { data }: VisualizationsStartDeps): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); setTypes(types); @@ -128,6 +130,7 @@ export class VisualizationsPlugin return { types, getSavedVisualizationsLoader: () => this.getSavedVisualizationsLoader(), + showNewVisModal, }; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap similarity index 99% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 2dcf3ed0ea06f..b1c9e37218c58 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -1154,7 +1154,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` > New Visualization @@ -1344,7 +1344,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` > Select a visualization type @@ -1718,7 +1718,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1`

Start creating your visualization by selecting a type for that visualization. @@ -2844,7 +2844,7 @@ exports[`NewVisModal should render as expected 1`] = ` > New Visualization @@ -3317,7 +3317,7 @@ exports[`NewVisModal should render as expected 1`] = ` > Select a visualization type @@ -3342,7 +3342,7 @@ exports[`NewVisModal should render as expected 1`] = `

Start creating your visualization by selecting a type for that visualization. diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/_dialog.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/_dialog.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/_dialog.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/_dialog.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/_index.scss b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/_index.scss rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/index.ts similarity index 94% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/index.ts index 7a3fc63af5259..55ac9ddfb5ca8 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export { NewVisModal } from './new_vis_modal'; export { showNewVisModal } from './show_new_vis'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx similarity index 96% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx index 0ef1b711eafc8..0701b5042a4bd 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.test.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.test.tsx @@ -19,15 +19,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; - -import { VisType } from '../../legacy_imports'; -import { TypesStart } from '../../../../../visualizations/public/np_ready/public/types'; - -jest.mock('../../legacy_imports', () => ({ - State: () => null, - AppState: () => null, -})); - +import { TypesStart, VisType } from '../types'; import { NewVisModal } from './new_vis_modal'; import { SavedObjectsStart } from 'kibana/public'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx similarity index 93% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx index 082fc3bc36b6b..fe66d2e56c611 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/new_vis_modal.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/new_vis_modal.tsx @@ -24,14 +24,9 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; -import { VisType } from '../../legacy_imports'; -import { VisualizeConstants } from '../visualize_constants'; import { SearchSelection } from './search_selection'; import { TypeSelection } from './type_selection'; -import { - TypesStart, - VisTypeAlias, -} from '../../../../../visualizations/public/np_ready/public/types'; +import { TypesStart, VisType, VisTypeAlias } from '../types'; import { UsageCollectionSetup } from '../../../../../../../plugins/usage_collection/public'; interface TypeSelectionProps { @@ -50,7 +45,9 @@ interface TypeSelectionState { visType?: VisType; } -const baseUrl = `#${VisualizeConstants.CREATE_PATH}?`; +// TODO: redirect logic is specific to visualise & dashboard +// but it is likely should be decoupled. e.g. handled by the container instead +const baseUrl = `#/visualize/create?`; class NewVisModal extends React.Component { public static defaultProps = { @@ -82,7 +79,7 @@ class NewVisModal extends React.Component void; @@ -42,13 +42,13 @@ export class SearchSelection extends React.Component { {' '} /{' '} @@ -59,7 +59,7 @@ export class SearchSelection extends React.Component { onChoose={this.props.onSearchSelected} showFilter noItemsMessage={i18n.translate( - 'kbn.visualize.newVisWizard.searchSelection.notFoundLabel', + 'visualizations.newVisWizard.searchSelection.notFoundLabel', { defaultMessage: 'No matching indices or saved searches found.', } @@ -69,7 +69,7 @@ export class SearchSelection extends React.Component { type: 'search', getIconForSavedObject: () => 'search', name: i18n.translate( - 'kbn.visualize.newVisWizard.searchSelection.savedObjectType.search', + 'visualizations.newVisWizard.searchSelection.savedObjectType.search', { defaultMessage: 'Saved search', } @@ -79,7 +79,7 @@ export class SearchSelection extends React.Component { type: 'index-pattern', getIconForSavedObject: () => 'indexPatternApp', name: i18n.translate( - 'kbn.visualize.newVisWizard.searchSelection.savedObjectType.indexPattern', + 'visualizations.newVisWizard.searchSelection.savedObjectType.indexPattern', { defaultMessage: 'Index pattern', } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/show_new_vis.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx similarity index 63% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/show_new_vis.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx index 567b7e861ad8e..8ca846ee39499 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/show_new_vis.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/show_new_vis.tsx @@ -21,27 +21,22 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { IUiSettingsClient, SavedObjectsStart } from 'kibana/public'; import { NewVisModal } from './new_vis_modal'; -import { TypesStart } from '../../../../../visualizations/public/np_ready/public/types'; -import { UsageCollectionSetup } from '../../../../../../../plugins/usage_collection/public'; +import { getHttp, getSavedObjects, getTypes, getUISettings, getUsageCollector } from '../services'; -interface ShowNewVisModalParams { +export interface ShowNewVisModalParams { editorParams?: string[]; + onClose?: () => void; } -export function showNewVisModal( - visTypeRegistry: TypesStart, - { editorParams = [] }: ShowNewVisModalParams = {}, - addBasePath: (path: string) => string, - uiSettings: IUiSettingsClient, - savedObjects: SavedObjectsStart, - usageCollection?: UsageCollectionSetup -) { +export function showNewVisModal({ editorParams = [], onClose }: ShowNewVisModalParams = {}) { const container = document.createElement('div'); - const onClose = () => { + const handleClose = () => { ReactDOM.unmountComponentAtNode(container); document.body.removeChild(container); + if (onClose) { + onClose(); + } }; document.body.appendChild(container); @@ -49,13 +44,13 @@ export function showNewVisModal( ); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/index.ts diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.test.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.test.tsx similarity index 97% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.test.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.test.tsx index a33a82c252fb3..138251beb9593 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.test.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.test.tsx @@ -49,7 +49,7 @@ describe('NewVisHelp', () => {

diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx similarity index 93% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx index 2f7effb7a33c8..5068f43952c4e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/new_vis_help.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/new_vis_help.tsx @@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { EuiText, EuiButton } from '@elastic/eui'; import { VisTypeAliasListEntry } from './type_selection'; -import { VisTypeAlias } from '../../../../../../visualizations/public'; +import { VisTypeAlias } from '../../types'; interface Props { promotedTypes: VisTypeAliasListEntry[]; @@ -33,7 +33,7 @@ export function NewVisHelp(props: Props) {

diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/type_selection.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx similarity index 90% rename from src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/type_selection.tsx rename to src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx index 44da7cc8f2c45..574f5b3cccc99 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/type_selection/type_selection.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/wizard/type_selection/type_selection.tsx @@ -34,13 +34,13 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { VisType } from '../../../legacy_imports'; + import { memoizeLast } from '../../../../../../visualizations/public/np_ready/public/legacy/memoize'; import { VisTypeAlias } from '../../../../../../visualizations/public'; import { NewVisHelp } from './new_vis_help'; import { VisHelpText } from './vis_help_text'; import { VisTypeIcon } from './vis_type_icon'; -import { TypesStart } from '../../../../../../visualizations/public/np_ready/public/types'; +import { VisType, TypesStart } from '../../types'; export interface VisTypeListEntry extends VisType { highlighted: boolean; @@ -85,7 +85,7 @@ class TypeSelection extends React.Component @@ -107,7 +107,7 @@ class TypeSelection extends React.Component {query && (

@@ -207,26 +207,26 @@ class TypeSelection extends React.Component !schema.isHiddenType(type)); const importableAndExportableTypes = getImportableAndExportableTypes({ kbnServer, visibleTypes }); @@ -99,7 +100,7 @@ export function savedObjectsMixin(kbnServer, server) { server.route(createResolveImportErrorsRoute(prereqs, server, importableAndExportableTypes)); server.route(createLogLegacyImportRoute()); - const serializer = new SavedObjectsSerializer(schema); + const serializer = kbnServer.newPlatform.start.core.savedObjects.createSerializer(); const createRepository = (callCluster, extraTypes = []) => { if (typeof callCluster !== 'function') { @@ -118,10 +119,9 @@ export function savedObjectsMixin(kbnServer, server) { return new SavedObjectsRepository({ index: config.get('kibana.index'), - config, migrator, mappings, - schema, + typeRegistry, serializer, allowedTypes, callCluster, diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index 691878cf66d27..9ca0374b959f6 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -22,6 +22,14 @@ import { savedObjectsMixin } from './saved_objects_mixin'; import { mockKibanaMigrator } from '../../../core/server/saved_objects/migrations/kibana/kibana_migrator.mock'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { savedObjectsClientProviderMock } from '../../../core/server/saved_objects/service/lib/scoped_client_provider.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { convertLegacyTypes } from '../../../core/server/saved_objects/utils'; +import { SavedObjectTypeRegistry } from '../../../core/server'; +import { coreMock } from '../../../core/server/mocks'; + +const mockConfig = { + get: jest.fn().mockReturnValue('anything'), +}; const savedObjectMappings = [ { @@ -61,7 +69,30 @@ const savedObjectMappings = [ }, ]; -const migrator = mockKibanaMigrator.create({ savedObjectMappings }); +const savedObjectSchemas = { + hiddentype: { + hidden: true, + }, + doc1: { + indexPattern: 'other-index', + }, +}; + +const savedObjectTypes = convertLegacyTypes( + { + savedObjectMappings, + savedObjectSchemas, + savedObjectMigrations: {}, + }, + mockConfig +); + +const typeRegistry = new SavedObjectTypeRegistry(); +savedObjectTypes.forEach(type => typeRegistry.registerType(type)); + +const migrator = mockKibanaMigrator.create({ + types: savedObjectTypes, +}); describe('Saved Objects Mixin', () => { let mockKbnServer; @@ -113,7 +144,17 @@ describe('Saved Objects Mixin', () => { }; mockKbnServer = { newPlatform: { - __internals: { kibanaMigrator: migrator, savedObjectsClientProvider: clientProvider }, + __internals: { + kibanaMigrator: migrator, + savedObjectsClientProvider: clientProvider, + typeRegistry, + }, + setup: { + core: coreMock.createSetup(), + }, + start: { + core: coreMock.createStart(), + }, }, server: mockServer, ready: () => {}, @@ -124,14 +165,7 @@ describe('Saved Objects Mixin', () => { }, uiExports: { savedObjectMappings, - savedObjectSchemas: { - hiddentype: { - hidden: true, - }, - doc1: { - indexPattern: 'other-index', - }, - }, + savedObjectSchemas, }, }; }); diff --git a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js index 0788afd5f74eb..12c3ca2acc3cd 100644 --- a/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js +++ b/src/legacy/ui/public/kbn_top_nav/kbn_top_nav.js @@ -92,6 +92,7 @@ export const createTopNavHelper = ({ TopNavMenu }) => reactDirective => { ['onClearSavedQuery', { watchDepth: 'reference' }], ['onSaved', { watchDepth: 'reference' }], ['onSavedQueryUpdated', { watchDepth: 'reference' }], + ['onSavedQueryIdChange', { watchDepth: 'reference' }], ['indexPatterns', { watchDepth: 'collection' }], ['filters', { watchDepth: 'collection' }], @@ -109,10 +110,14 @@ export const createTopNavHelper = ({ TopNavMenu }) => reactDirective => { 'screenTitle', 'dateRangeFrom', 'dateRangeTo', + 'savedQueryId', 'isRefreshPaused', 'refreshInterval', 'disableAutoFocus', 'showAutoRefreshOnly', + + // temporary flag to use the stateful components + 'useDefaultBehaviors', ]); }; diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts index cf24b0e9675fa..6f6b4be86f58d 100644 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts @@ -43,7 +43,7 @@ export const pluginsMock = { uiActions: uiActionsPluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), advancedSettings: advancedSettingsMock.createSetupContract(), - kibana_legacy: kibanaLegacyPluginMock.createSetupContract(), + kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), }), createStart: () => ({ data: dataPluginMock.createStartContract(), @@ -55,7 +55,7 @@ export const pluginsMock = { uiActions: uiActionsPluginMock.createStartContract(), management: managementPluginMock.createStartContract(), advancedSettings: advancedSettingsMock.createStartContract(), - kibana_legacy: kibanaLegacyPluginMock.createStartContract(), + kibanaLegacy: kibanaLegacyPluginMock.createStartContract(), }), }; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 7f4f67ebb06d3..3cc33504d3daa 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -120,10 +120,10 @@ export const npSetup = { share: { register: () => {}, }, - dev_tools: { + devTools: { register: () => {}, }, - kibana_legacy: { + kibanaLegacy: { registerLegacyApp: () => {}, forwardApp: () => {}, config: { @@ -203,10 +203,10 @@ export const npStart = { registerRenderer: sinon.fake(), registerType: sinon.fake(), }, - dev_tools: { + devTools: { getSortedDevTools: () => [], }, - kibana_legacy: { + kibanaLegacy: { getApps: () => [], getForwards: () => [], config: { diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 62abe2eb9b5ba..e300ce4a0caf8 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -18,7 +18,7 @@ */ import { IScope } from 'angular'; -import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; +import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; import { LegacyCoreSetup, LegacyCoreStart, App, AppMountDeprecated } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; @@ -52,10 +52,10 @@ export interface PluginsSetup { expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; - uiActions: IUiActionsSetup; + uiActions: UiActionsSetup; navigation: NavigationPublicPluginSetup; - dev_tools: DevToolsSetup; - kibana_legacy: KibanaLegacySetup; + devTools: DevToolsSetup; + kibanaLegacy: KibanaLegacySetup; share: SharePluginSetup; usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; @@ -70,10 +70,10 @@ export interface PluginsStart { expressions: ReturnType; home: HomePublicPluginStart; inspector: InspectorStart; - uiActions: IUiActionsStart; + uiActions: UiActionsStart; navigation: NavigationPublicPluginStart; - dev_tools: DevToolsStart; - kibana_legacy: KibanaLegacyStart; + devTools: DevToolsStart; + kibanaLegacy: KibanaLegacyStart; share: SharePluginStart; management: ManagementStart; advancedSettings: AdvancedSettingsStart; diff --git a/src/legacy/ui/public/saved_objects/_index.scss b/src/legacy/ui/public/saved_objects/_index.scss index 50a192b6a7b17..89cda29f67744 100644 --- a/src/legacy/ui/public/saved_objects/_index.scss +++ b/src/legacy/ui/public/saved_objects/_index.scss @@ -1 +1 @@ -@import '../../../../plugins/kibana_react/public/saved_objects/index'; +@import '../../../../plugins/saved_objects/public/index'; diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json index 18f7eb06e98ed..57de385ba565c 100644 --- a/src/plugins/console/kibana.json +++ b/src/plugins/console/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["dev_tools", "home"], + "requiredPlugins": ["devTools", "home"], "optionalPlugins": ["usageCollection"] } diff --git a/src/plugins/console/public/application/components/index.ts b/src/plugins/console/public/application/components/index.ts index eccde899a2640..d9a8aa9328b73 100644 --- a/src/plugins/console/public/application/components/index.ts +++ b/src/plugins/console/public/application/components/index.ts @@ -17,6 +17,7 @@ * under the License. */ +export { NetworkRequestStatusBar } from './network_request_status_bar'; export { SomethingWentWrongCallout } from './something_went_wrong_callout'; export { TopNavMenuItem, TopNavMenu } from './top_nav_menu'; export { ConsoleMenu } from './console_menu'; diff --git a/src/plugins/kibana_react/public/saved_objects/index.ts b/src/plugins/console/public/application/components/network_request_status_bar/index.ts similarity index 90% rename from src/plugins/kibana_react/public/saved_objects/index.ts rename to src/plugins/console/public/application/components/network_request_status_bar/index.ts index ade80d2cd2a92..ce214c1cdfffa 100644 --- a/src/plugins/kibana_react/public/saved_objects/index.ts +++ b/src/plugins/console/public/application/components/network_request_status_bar/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './saved_object_finder'; -export * from './saved_object_save_modal'; +export { NetworkRequestStatusBar } from './network_request_status_bar'; diff --git a/src/plugins/console/public/application/components/network_request_status_bar/network_request_status_bar.tsx b/src/plugins/console/public/application/components/network_request_status_bar/network_request_status_bar.tsx new file mode 100644 index 0000000000000..6915ff15f374d --- /dev/null +++ b/src/plugins/console/public/application/components/network_request_status_bar/network_request_status_bar.tsx @@ -0,0 +1,133 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, EuiToolTip } from '@elastic/eui'; + +export interface Props { + requestInProgress: boolean; + requestResult?: { + // Status code of the request, e.g., 200 + statusCode: number; + + // Status text of the request, e.g., OK + statusText: string; + + // Method of the request, e.g., GET + method: string; + + // The path of endpoint that was called, e.g., /_search + endpoint: string; + + // The time, in milliseconds, that the last request took + timeElapsedMs: number; + }; +} + +const mapStatusCodeToBadgeColor = (statusCode: number) => { + if (statusCode <= 199) { + return 'default'; + } + + if (statusCode <= 299) { + return 'secondary'; + } + + if (statusCode <= 399) { + return 'primary'; + } + + if (statusCode <= 499) { + return 'warning'; + } + + return 'danger'; +}; + +export const NetworkRequestStatusBar: FunctionComponent = ({ + requestInProgress, + requestResult, +}) => { + let content: React.ReactNode = null; + + if (requestInProgress) { + content = ( + + + {i18n.translate('console.requestInProgressBadgeText', { + defaultMessage: 'Request in progress', + })} + + + ); + } else if (requestResult) { + const { endpoint, method, statusCode, statusText, timeElapsedMs } = requestResult; + + content = ( + <> + + {`${method} ${ + endpoint.startsWith('/') ? endpoint : '/' + endpoint + }`}
+ } + > + + {/* Use   to ensure that no matter the width we don't allow line breaks */} + {statusCode} - {statusText} + + + + + + {i18n.translate('console.requestTimeElapasedBadgeTooltipContent', { + defaultMessage: 'Time Elapsed', + })} + + } + > + + + {timeElapsedMs} {'ms'} + + + + + + ); + } + + return ( + + {content} + + ); +}; diff --git a/src/plugins/console/public/application/containers/editor/editor.tsx b/src/plugins/console/public/application/containers/editor/editor.tsx index 5c7fe293651fb..0bfe837f2cd90 100644 --- a/src/plugins/console/public/application/containers/editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/editor.tsx @@ -17,14 +17,15 @@ * under the License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, memo } from 'react'; import { debounce } from 'lodash'; +import { EuiProgress } from '@elastic/eui'; import { EditorContentSpinner } from '../../components'; import { Panel, PanelsContainer } from '../../../../../kibana_react/public'; import { Editor as EditorUI, EditorOutput } from './legacy/console_editor'; import { StorageKeys } from '../../../services'; -import { useEditorReadContext, useServicesContext } from '../../contexts'; +import { useEditorReadContext, useServicesContext, useRequestReadContext } from '../../contexts'; const INITIAL_PANEL_WIDTH = 50; const PANEL_MIN_WIDTH = '100px'; @@ -33,12 +34,13 @@ interface Props { loading: boolean; } -export const Editor = ({ loading }: Props) => { +export const Editor = memo(({ loading }: Props) => { const { services: { storage }, } = useServicesContext(); const { currentTextObject } = useEditorReadContext(); + const { requestInFlight } = useRequestReadContext(); const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ INITIAL_PANEL_WIDTH, @@ -55,23 +57,30 @@ export const Editor = ({ loading }: Props) => { if (!currentTextObject) return null; return ( - - - {loading ? ( - - ) : ( - - )} - - - {loading ? : } - - + <> + {requestInFlight ? ( +
+ +
+ ) : null} + + + {loading ? ( + + ) : ( + + )} + + + {loading ? : } + + + ); -}; +}); diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index 759e3dbafb39c..b3e966ddffa4c 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -211,14 +211,14 @@ function EditorUI({ initialTextValue }: EditorProps) {