diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index f206a914aef97..dc6804b0630bd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,4 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | - +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 8bd87c2f6ea35..69cfb818561e5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -23,9 +23,11 @@ export interface SavedObjectsFindOptions | [namespaces](./kibana-plugin-core-public.savedobjectsfindoptions.namespaces.md) | string[] | | | [page](./kibana-plugin-core-public.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-core-public.savedobjectsfindoptions.perpage.md) | number | | +| [pit](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams | Search against a specific Point In Time (PIT) that you've opened with . | | [preference](./kibana-plugin-core-public.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | | [rootSearchFields](./kibana-plugin-core-public.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-public.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) | unknown[] | Use the sort values from the previous page to retrieve the next page of results. | | [searchFields](./kibana-plugin-core-public.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | string | | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.pit.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.pit.md new file mode 100644 index 0000000000000..2284a4d8d210d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.pit.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [pit](./kibana-plugin-core-public.savedobjectsfindoptions.pit.md) + +## SavedObjectsFindOptions.pit property + +Search against a specific Point In Time (PIT) that you've opened with . + +Signature: + +```typescript +pit?: SavedObjectsPitParams; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md new file mode 100644 index 0000000000000..99ca2c34e77be --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [searchAfter](./kibana-plugin-core-public.savedobjectsfindoptions.searchafter.md) + +## SavedObjectsFindOptions.searchAfter property + +Use the sort values from the previous page to retrieve the next page of results. + +Signature: + +```typescript +searchAfter?: unknown[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 5fe5eda7a8172..1791335d58fef 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -155,6 +155,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsCheckConflictsResponse](./kibana-plugin-core-server.savedobjectscheckconflictsresponse.md) | | | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | +| [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) | | | [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | @@ -188,6 +189,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsMappingProperties](./kibana-plugin-core-server.savedobjectsmappingproperties.md) | Describe the fields of a [saved object type](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md). | | [SavedObjectsMigrationLogger](./kibana-plugin-core-server.savedobjectsmigrationlogger.md) | | | [SavedObjectsMigrationVersion](./kibana-plugin-core-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. | +| [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) | | +| [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md) | | +| [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) | | | [SavedObjectsRawDoc](./kibana-plugin-core-server.savedobjectsrawdoc.md) | A raw document as represented directly in the saved object index. | | [SavedObjectsRawDocParseOptions](./kibana-plugin-core-server.savedobjectsrawdocparseoptions.md) | Options that can be specified when using the saved objects serializer to parse a raw document. | | [SavedObjectsRemoveReferencesToOptions](./kibana-plugin-core-server.savedobjectsremovereferencestooptions.md) | | @@ -301,6 +305,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | +| [SavedObjectsClosePointInTimeOptions](./kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md) | | | [SavedObjectsExportTransform](./kibana-plugin-core-server.savedobjectsexporttransform.md) | Transformation function used to mutate the exported objects of the associated type.A type's export transform function will be executed once per user-initiated export, for all objects of that type. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-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 | | [SavedObjectsImportHook](./kibana-plugin-core-server.savedobjectsimporthook.md) | A hook associated with a specific saved object type, that will be invoked during the import process. The hook will have access to the objects of the registered type.Currently, the only supported feature for import hooks is to return warnings to be displayed in the UI when the import succeeds. The only interactions the hook can have with the import process is via the hook's response. Mutating the objects inside the hook's code will have no effect. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md new file mode 100644 index 0000000000000..dc765260a08ca --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.closepointintime.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [closePointInTime](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) + +## SavedObjectsClient.closePointInTime() method + +Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). + +Signature: + +```typescript +closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| id | string | | +| options | SavedObjectsClosePointInTimeOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md index da1f4d029ea2b..887f7f7d93a87 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.md @@ -30,11 +30,13 @@ The constructor for this class is marked as internal. Third-party code should no | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsclient.bulkupdate.md) | | Bulk Updates multiple SavedObject at once | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsclient.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | +| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsclient.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.create.md) | | Persists a SavedObject | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsclient.deletefromnamespaces.md) | | Removes namespaces from a SavedObject | | [find(options)](./kibana-plugin-core-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.get.md) | | Retrieves a single object | +| [openPointInTimeForType(type, options)](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsclient.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsclient.update.md) | | Updates an SavedObject | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md new file mode 100644 index 0000000000000..56c1d6d1ddc33 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) > [openPointInTimeForType](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md) + +## SavedObjectsClient.openPointInTimeForType() method + +Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned `id` can then be passed to [SavedObjectsClient.find()](./kibana-plugin-core-server.savedobjectsclient.find.md) to search against that PIT. + +Signature: + +```typescript +openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointInTimeOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | string[] | | +| options | SavedObjectsOpenPointInTimeOptions | | + +Returns: + +`Promise` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md new file mode 100644 index 0000000000000..27432a8805b06 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeOptions](./kibana-plugin-core-server.savedobjectsclosepointintimeoptions.md) + +## SavedObjectsClosePointInTimeOptions type + + +Signature: + +```typescript +export declare type SavedObjectsClosePointInTimeOptions = SavedObjectsBaseOptions; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md new file mode 100644 index 0000000000000..43ecd1298d5d9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) + +## SavedObjectsClosePointInTimeResponse interface + + +Signature: + +```typescript +export interface SavedObjectsClosePointInTimeResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md) | number | The number of search contexts that have been successfully closed. | +| [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md) | boolean | If true, all search contexts associated with the PIT id are successfully closed. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md new file mode 100644 index 0000000000000..b64932fcee8f6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) > [num\_freed](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.num_freed.md) + +## SavedObjectsClosePointInTimeResponse.num\_freed property + +The number of search contexts that have been successfully closed. + +Signature: + +```typescript +num_freed: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md new file mode 100644 index 0000000000000..225a549a4cf59 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) > [succeeded](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.succeeded.md) + +## SavedObjectsClosePointInTimeResponse.succeeded property + +If true, all search contexts associated with the PIT id are successfully closed. + +Signature: + +```typescript +succeeded: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md index 5e959bbee7beb..3f3d708c590ee 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter._constructor_.md @@ -9,10 +9,11 @@ Constructs a new instance of the `SavedObjectsExporter` class Signature: ```typescript -constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: { +constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, }: { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; exportSizeLimit: number; + logger: Logger; }); ``` @@ -20,5 +21,5 @@ constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: { | Parameter | Type | Description | | --- | --- | --- | -| { savedObjectsClient, typeRegistry, exportSizeLimit, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
} | | +| { savedObjectsClient, typeRegistry, exportSizeLimit, logger, } | {
savedObjectsClient: SavedObjectsClientContract;
typeRegistry: ISavedObjectTypeRegistry;
exportSizeLimit: number;
logger: Logger;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md index 727108b824c84..ce23e91633b07 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporter.md @@ -15,7 +15,7 @@ export declare class SavedObjectsExporter | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter class | +| [(constructor)({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, })](./kibana-plugin-core-server.savedobjectsexporter._constructor_.md) | | Constructs a new instance of the SavedObjectsExporter class | ## Properties diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index d393d579dbdd2..6f7c05ea469bc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -23,9 +23,11 @@ export interface SavedObjectsFindOptions | [namespaces](./kibana-plugin-core-server.savedobjectsfindoptions.namespaces.md) | string[] | | | [page](./kibana-plugin-core-server.savedobjectsfindoptions.page.md) | number | | | [perPage](./kibana-plugin-core-server.savedobjectsfindoptions.perpage.md) | number | | +| [pit](./kibana-plugin-core-server.savedobjectsfindoptions.pit.md) | SavedObjectsPitParams | Search against a specific Point In Time (PIT) that you've opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). | | [preference](./kibana-plugin-core-server.savedobjectsfindoptions.preference.md) | string | An optional ES preference value to be used for the query \* | | [rootSearchFields](./kibana-plugin-core-server.savedobjectsfindoptions.rootsearchfields.md) | string[] | The fields to perform the parsed query against. Unlike the searchFields argument, these are expected to be root fields and will not be modified. If used in conjunction with searchFields, both are concatenated together. | | [search](./kibana-plugin-core-server.savedobjectsfindoptions.search.md) | string | Search documents using the Elasticsearch Simple Query String syntax. See Elasticsearch Simple Query String query argument for more information | +| [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) | unknown[] | Use the sort values from the previous page to retrieve the next page of results. | | [searchFields](./kibana-plugin-core-server.savedobjectsfindoptions.searchfields.md) | string[] | The fields to perform the parsed query against. See Elasticsearch Simple Query String fields argument for more information | | [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.pit.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.pit.md new file mode 100644 index 0000000000000..fac333227088c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.pit.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [pit](./kibana-plugin-core-server.savedobjectsfindoptions.pit.md) + +## SavedObjectsFindOptions.pit property + +Search against a specific Point In Time (PIT) that you've opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). + +Signature: + +```typescript +pit?: SavedObjectsPitParams; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md new file mode 100644 index 0000000000000..6364370948976 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [searchAfter](./kibana-plugin-core-server.savedobjectsfindoptions.searchafter.md) + +## SavedObjectsFindOptions.searchAfter property + +Use the sort values from the previous page to retrieve the next page of results. + +Signature: + +```typescript +searchAfter?: unknown[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md index 4ed069d1598fe..fd56e8ce40e24 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.md @@ -20,6 +20,7 @@ export interface SavedObjectsFindResponse | --- | --- | --- | | [page](./kibana-plugin-core-server.savedobjectsfindresponse.page.md) | number | | | [per\_page](./kibana-plugin-core-server.savedobjectsfindresponse.per_page.md) | number | | +| [pit\_id](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md) | string | | | [saved\_objects](./kibana-plugin-core-server.savedobjectsfindresponse.saved_objects.md) | Array<SavedObjectsFindResult<T>> | | | [total](./kibana-plugin-core-server.savedobjectsfindresponse.total.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md new file mode 100644 index 0000000000000..dc4f9b509d606 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResponse](./kibana-plugin-core-server.savedobjectsfindresponse.md) > [pit\_id](./kibana-plugin-core-server.savedobjectsfindresponse.pit_id.md) + +## SavedObjectsFindResponse.pit\_id property + +Signature: + +```typescript +pit_id?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md index e455074a7d11b..0f8e9c59236bb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.md @@ -16,4 +16,5 @@ export interface SavedObjectsFindResult extends SavedObject | Property | Type | Description | | --- | --- | --- | | [score](./kibana-plugin-core-server.savedobjectsfindresult.score.md) | number | The Elasticsearch _score of this result. | +| [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) | unknown[] | The Elasticsearch sort value of this result. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md new file mode 100644 index 0000000000000..3cc02c404c8d7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindResult](./kibana-plugin-core-server.savedobjectsfindresult.md) > [sort](./kibana-plugin-core-server.savedobjectsfindresult.sort.md) + +## SavedObjectsFindResult.sort property + +The Elasticsearch `sort` value of this result. + +Signature: + +```typescript +sort?: unknown[]; +``` + +## Remarks + +This can be passed directly to the `searchAfter` param in the [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) in order to page through large numbers of hits. It is recommended you use this alongside a Point In Time (PIT) that was opened with [SavedObjectsClient.openPointInTimeForType()](./kibana-plugin-core-server.savedobjectsclient.openpointintimefortype.md). + +## Example + + +```ts +const { id } = await savedObjectsClient.openPointInTimeForType('visualization'); +const page1 = await savedObjectsClient.find({ + type: 'visualization', + sortField: 'updated_at', + sortOrder: 'asc', + pit, +}); +const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; +const page2 = await savedObjectsClient.find({ + type: 'visualization', + sortField: 'updated_at', + sortOrder: 'asc', + pit: { id: page1.pit_id }, + searchAfter: lastHit.sort, +}); +await savedObjectsClient.closePointInTime(page2.pit_id); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md new file mode 100644 index 0000000000000..57752318cb96a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) > [keepAlive](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md) + +## SavedObjectsOpenPointInTimeOptions.keepAlive property + +Optionally specify how long ES should keep the PIT alive until the next request. Defaults to `5m`. + +Signature: + +```typescript +keepAlive?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md new file mode 100644 index 0000000000000..46516be2329e9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) + +## SavedObjectsOpenPointInTimeOptions interface + + +Signature: + +```typescript +export interface SavedObjectsOpenPointInTimeOptions extends SavedObjectsBaseOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [keepAlive](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.keepalive.md) | string | Optionally specify how long ES should keep the PIT alive until the next request. Defaults to 5m. | +| [preference](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md) | string | An optional ES preference value to be used for the query. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md new file mode 100644 index 0000000000000..7a9f3a49e8663 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeOptions](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.md) > [preference](./kibana-plugin-core-server.savedobjectsopenpointintimeoptions.preference.md) + +## SavedObjectsOpenPointInTimeOptions.preference property + +An optional ES preference value to be used for the query. + +Signature: + +```typescript +preference?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md new file mode 100644 index 0000000000000..66387e5b3b89f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md) > [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md) + +## SavedObjectsOpenPointInTimeResponse.id property + +PIT ID returned from ES. + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md new file mode 100644 index 0000000000000..c4be2692763a5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsOpenPointInTimeResponse](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.md) + +## SavedObjectsOpenPointInTimeResponse interface + + +Signature: + +```typescript +export interface SavedObjectsOpenPointInTimeResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsopenpointintimeresponse.id.md) | string | PIT ID returned from ES. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.id.md new file mode 100644 index 0000000000000..cb4d4a65727d7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) > [id](./kibana-plugin-core-server.savedobjectspitparams.id.md) + +## SavedObjectsPitParams.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.keepalive.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.keepalive.md new file mode 100644 index 0000000000000..1011a908f210a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.keepalive.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) > [keepAlive](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md) + +## SavedObjectsPitParams.keepAlive property + +Signature: + +```typescript +keepAlive?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md new file mode 100644 index 0000000000000..7bffca7cda281 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectspitparams.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsPitParams](./kibana-plugin-core-server.savedobjectspitparams.md) + +## SavedObjectsPitParams interface + + +Signature: + +```typescript +export interface SavedObjectsPitParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectspitparams.id.md) | string | | +| [keepAlive](./kibana-plugin-core-server.savedobjectspitparams.keepalive.md) | string | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md new file mode 100644 index 0000000000000..8f9dca35fa362 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.closepointintime.md @@ -0,0 +1,58 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [closePointInTime](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) + +## SavedObjectsRepository.closePointInTime() method + +Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using `openPointInTimeForType`. + +Signature: + +```typescript +closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| id | string | | +| options | SavedObjectsClosePointInTimeOptions | | + +Returns: + +`Promise` + +{promise} - [SavedObjectsClosePointInTimeResponse](./kibana-plugin-core-server.savedobjectsclosepointintimeresponse.md) + +## Remarks + +While the `keepAlive` that is provided will cause a PIT to automatically close, it is highly recommended to explicitly close a PIT when you are done with it in order to avoid consuming unneeded resources in Elasticsearch. + +## Example + + +```ts +const repository = coreStart.savedObjects.createInternalRepository(); + +const { id } = await repository.openPointInTimeForType( + type: 'index-pattern', + { keepAlive: '2m' }, +); + +const response = await repository.find({ + type: 'index-pattern', + search: 'foo*', + sortField: 'name', + sortOrder: 'desc', + pit: { + id: 'abc123', + keepAlive: '2m', + }, + searchAfter: [1234, 'abcd'], +}); + +await repository.closePointInTime(response.pit_id); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 4d13fea12572c..632d9c279cb88 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -20,6 +20,7 @@ export declare class SavedObjectsRepository | [bulkGet(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkget.md) | | Returns an array of objects by id | | [bulkUpdate(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.bulkupdate.md) | | Updates multiple objects in bulk | | [checkConflicts(objects, options)](./kibana-plugin-core-server.savedobjectsrepository.checkconflicts.md) | | Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten. | +| [closePointInTime(id, options)](./kibana-plugin-core-server.savedobjectsrepository.closepointintime.md) | | Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the Elasticsearch client, and is included in the Saved Objects Client as a convenience for consumers who are using openPointInTimeForType. | | [create(type, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.create.md) | | Persists an object | | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | @@ -27,6 +28,7 @@ export declare class SavedObjectsRepository | [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFields, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id. | +| [openPointInTimeForType(type, { keepAlive, preference })](./kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md) | | Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned id can then be passed to SavedObjects.find to search against that PIT. | | [removeReferencesTo(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.removereferencesto.md) | | Updates all objects containing a reference to the given {type, id} tuple to remove the said reference. | | [resolve(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.resolve.md) | | Resolves a single object, using any legacy URL alias if it exists | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md new file mode 100644 index 0000000000000..63956ebee68f7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md @@ -0,0 +1,57 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) > [openPointInTimeForType](./kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md) + +## SavedObjectsRepository.openPointInTimeForType() method + +Opens a Point In Time (PIT) against the indices for the specified Saved Object types. The returned `id` can then be passed to `SavedObjects.find` to search against that PIT. + +Signature: + +```typescript +openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | string[] | | +| { keepAlive, preference } | SavedObjectsOpenPointInTimeOptions | | + +Returns: + +`Promise` + +{promise} - { id: string } + +## Example + + +```ts +const repository = coreStart.savedObjects.createInternalRepository(); + +const { id } = await repository.openPointInTimeForType( + type: 'index-pattern', + { keepAlive: '2m' }, +); +const page1 = await savedObjectsClient.find({ + type: 'visualization', + sortField: 'updated_at', + sortOrder: 'asc', + pit, +}); + +const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; +const page2 = await savedObjectsClient.find({ + type: 'visualization', + sortField: 'updated_at', + sortOrder: 'asc', + pit: { id: page1.pit_id }, + searchAfter: lastHit.sort, +}); + +await savedObjectsClient.closePointInTime(page2.pit_id); + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md index 1629e77425525..599c4e3ad6319 100644 --- a/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.hits.md @@ -22,7 +22,7 @@ hits: { highlight?: any; inner_hits?: any; matched_queries?: string[]; - sort?: string[]; + sort?: unknown[]; }>; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md index b53cbf0d87f24..cbaab4632014d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.searchresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.md @@ -18,7 +18,8 @@ export interface SearchResponse | [\_scroll\_id](./kibana-plugin-core-server.searchresponse._scroll_id.md) | string | | | [\_shards](./kibana-plugin-core-server.searchresponse._shards.md) | ShardsResponse | | | [aggregations](./kibana-plugin-core-server.searchresponse.aggregations.md) | any | | -| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | {
total: number;
max_score: number;
hits: Array<{
_index: string;
_type: string;
_id: string;
_score: number;
_source: T;
_version?: number;
_explanation?: Explanation;
fields?: any;
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: string[];
}>;
} | | +| [hits](./kibana-plugin-core-server.searchresponse.hits.md) | {
total: number;
max_score: number;
hits: Array<{
_index: string;
_type: string;
_id: string;
_score: number;
_source: T;
_version?: number;
_explanation?: Explanation;
fields?: any;
highlight?: any;
inner_hits?: any;
matched_queries?: string[];
sort?: unknown[];
}>;
} | | +| [pit\_id](./kibana-plugin-core-server.searchresponse.pit_id.md) | string | | | [timed\_out](./kibana-plugin-core-server.searchresponse.timed_out.md) | boolean | | | [took](./kibana-plugin-core-server.searchresponse.took.md) | number | | diff --git a/docs/development/core/server/kibana-plugin-core-server.searchresponse.pit_id.md b/docs/development/core/server/kibana-plugin-core-server.searchresponse.pit_id.md new file mode 100644 index 0000000000000..f214bc0538045 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.searchresponse.pit_id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SearchResponse](./kibana-plugin-core-server.searchresponse.md) > [pit\_id](./kibana-plugin-core-server.searchresponse.pit_id.md) + +## SearchResponse.pit\_id property + +Signature: + +```typescript +pit_id?: string; +``` diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc index 12a87b1422c5c..b9fc0c9c4ac46 100644 --- a/docs/user/security/audit-logging.asciidoc +++ b/docs/user/security/audit-logging.asciidoc @@ -85,6 +85,10 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is creating a saved object. | `failure` | User is not authorized to create a saved object. +.2+| `saved_object_open_point_in_time` +| `unknown` | User is creating a Point In Time to use when querying saved objects. +| `failure` | User is not authorized to create a Point In Time for the provided saved object types. + .2+| `connector_create` | `unknown` | User is creating a connector. | `failure` | User is not authorized to create a connector. @@ -171,6 +175,10 @@ Refer to the corresponding {es} logs for potential write errors. | `unknown` | User is deleting a saved object. | `failure` | User is not authorized to delete a saved object. +.2+| `saved_object_close_point_in_time` +| `unknown` | User is deleting a Point In Time that was used to query saved objects. +| `failure` | User is not authorized to delete a Point In Time. + .2+| `connector_delete` | `unknown` | User is deleting a connector. | `failure` | User is not authorized to delete a connector. diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 79fc3db86e066..a1475985af8df 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -37,7 +37,6 @@ module.exports = { '\\.ace\\.worker.js$': '/packages/kbn-test/target/jest/mocks/worker_module_mock.js', '\\.editor\\.worker.js$': '/packages/kbn-test/target/jest/mocks/worker_module_mock.js', '^(!!)?file-loader!': '/packages/kbn-test/target/jest/mocks/file_mock.js', - '^fixtures/(.*)': '/src/fixtures/$1', '^src/core/(.*)': '/src/core/$1', '^src/plugins/(.*)': '/src/plugins/$1', }, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 2922606ac3e1e..f646972a20f8d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1207,9 +1207,13 @@ export interface SavedObjectsFindOptions { page?: number; // (undocumented) perPage?: number; + // Warning: (ae-forgotten-export) The symbol "SavedObjectsPitParams" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: No member was found with name "openPointInTimeForType" + pit?: SavedObjectsPitParams; preference?: string; rootSearchFields?: string[]; search?: string; + searchAfter?: unknown[]; searchFields?: string[]; // (undocumented) sortField?: string; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 9c0a44b2d3da0..44466025de7e3 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -21,12 +21,14 @@ import { import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; +type PromiseType> = T extends Promise ? U : never; + type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, - 'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap' + 'pit' | 'rootSearchFields' | 'searchAfter' | 'sortOrder' | 'typeToNamespacesMap' >; -type PromiseType> = T extends Promise ? U : never; +type SavedObjectsFindResponse = Omit>, 'pit_id'>; /** @public */ export interface SavedObjectsCreateOptions { @@ -345,10 +347,7 @@ export class SavedObjectsClient { query, }); return request.then((resp) => { - return renameKeys< - PromiseType>, - SavedObjectsFindResponsePublic - >( + return renameKeys( { saved_objects: 'savedObjects', total: 'total', diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index 1a0706917b5dd..21a599e45da01 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -105,6 +105,7 @@ const createStartContractMock = () => { loggersConfiguredCount: 0, }, savedObjects: { + customIndex: false, maxImportExportSizeBytes: 10000, maxImportPayloadBytes: 26214400, }, diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 5a9a68c9e4ece..9086d73b77807 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -238,6 +238,7 @@ describe('CoreUsageDataService', () => { "loggersConfiguredCount": 0, }, "savedObjects": Object { + "customIndex": false, "maxImportExportSizeBytes": 10000, "maxImportPayloadBytes": 26214400, }, diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index a4c6c6e8c66f4..bd5f23b1c09bc 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -59,6 +59,19 @@ const kibanaOrTaskManagerIndex = (index: string, kibanaConfigIndex: string) => { return index === kibanaConfigIndex ? '.kibana' : '.kibana_task_manager'; }; +/** + * This is incredibly hacky... The config service doesn't allow you to determine + * whether or not a config value has been changed from the default value, and the + * default value is defined in legacy code. + * + * This will be going away in 8.0, so please look away for a few months + * + * @param index The `kibana.index` setting from the `kibana.yml` + */ +const isCustomIndex = (index: string) => { + return index !== '.kibana'; +}; + export class CoreUsageDataService implements CoreService { private logger: Logger; private elasticsearchConfig?: ElasticsearchConfigType; @@ -220,6 +233,7 @@ export class CoreUsageDataService implements CoreService { highlight?: any; inner_hits?: any; matched_queries?: string[]; - sort?: string[]; + sort?: unknown[]; }>; }; aggregations?: any; + pit_id?: string; } /** diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 6f478004c204e..dac2d210eb395 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -260,6 +260,8 @@ export { SavedObjectsClientWrapperOptions, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, + SavedObjectsClosePointInTimeOptions, + SavedObjectsClosePointInTimeResponse, SavedObjectsCreateOptions, SavedObjectsErrorHelpers, SavedObjectsExportResultDetails, @@ -277,6 +279,8 @@ export { SavedObjectsImportUnsupportedTypeError, SavedObjectMigrationContext, SavedObjectsMigrationLogger, + SavedObjectsOpenPointInTimeOptions, + SavedObjectsOpenPointInTimeResponse, SavedObjectsRawDoc, SavedObjectsRawDocParseOptions, SavedObjectSanitizedDoc, @@ -373,6 +377,7 @@ export { SavedObjectsClientContract, SavedObjectsFindOptions, SavedObjectsFindOptionsReference, + SavedObjectsPitParams, SavedObjectsMigrationVersion, } from './types'; diff --git a/src/core/server/saved_objects/export/point_in_time_finder.test.ts b/src/core/server/saved_objects/export/point_in_time_finder.test.ts new file mode 100644 index 0000000000000..cd79c7a4b81e5 --- /dev/null +++ b/src/core/server/saved_objects/export/point_in_time_finder.test.ts @@ -0,0 +1,321 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; +import { loggerMock, MockedLogger } from '../../logging/logger.mock'; +import { SavedObjectsFindOptions } from '../types'; +import { SavedObjectsFindResult } from '../service'; + +import { createPointInTimeFinder } from './point_in_time_finder'; + +const mockHits = [ + { + id: '2', + type: 'search', + attributes: {}, + score: 1, + references: [ + { + name: 'name', + type: 'visualization', + id: '1', + }, + ], + sort: [], + }, + { + id: '1', + type: 'visualization', + attributes: {}, + score: 1, + references: [], + sort: [], + }, +]; + +describe('createPointInTimeFinder()', () => { + let logger: MockedLogger; + let savedObjectsClient: ReturnType; + + beforeEach(() => { + logger = loggerMock.create(); + savedObjectsClient = savedObjectsClientMock.create(); + }); + + describe('#find', () => { + test('throws if a PIT is already open', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'abc123', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: mockHits, + pit_id: 'abc123', + per_page: 1, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: mockHits, + pit_id: 'abc123', + per_page: 1, + page: 1, + }); + + const findOptions: SavedObjectsFindOptions = { + type: ['visualization'], + search: 'foo*', + perPage: 1, + }; + + const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + await finder.find().next(); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + savedObjectsClient.find.mockClear(); + + expect(async () => { + await finder.find().next(); + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"Point In Time has already been opened for this finder instance. Please call \`close()\` before calling \`find()\` again."` + ); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(0); + }); + + test('works with a single page of results', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'abc123', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: mockHits, + pit_id: 'abc123', + per_page: 2, + page: 0, + }); + + const findOptions: SavedObjectsFindOptions = { + type: ['visualization'], + search: 'foo*', + }; + + const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const hits: SavedObjectsFindResult[] = []; + for await (const result of finder.find()) { + hits.push(...result.saved_objects); + } + + expect(hits.length).toBe(2); + expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }), + sortField: 'updated_at', + sortOrder: 'desc', + type: ['visualization'], + }) + ); + }); + + test('works with multiple pages of results', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'abc123', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [mockHits[0]], + pit_id: 'abc123', + per_page: 1, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [mockHits[1]], + pit_id: 'abc123', + per_page: 1, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [], + per_page: 1, + pit_id: 'abc123', + page: 0, + }); + + const findOptions: SavedObjectsFindOptions = { + type: ['visualization'], + search: 'foo*', + perPage: 1, + }; + + const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const hits: SavedObjectsFindResult[] = []; + for await (const result of finder.find()) { + hits.push(...result.saved_objects); + } + + expect(hits.length).toBe(2); + expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); + // called 3 times since we need a 3rd request to check if we + // are done paginating through results. + expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); + expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }), + sortField: 'updated_at', + sortOrder: 'desc', + type: ['visualization'], + }) + ); + }); + }); + + describe('#close', () => { + test('calls closePointInTime with correct ID', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'test', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1, + saved_objects: [mockHits[0]], + pit_id: 'test', + per_page: 1, + page: 0, + }); + + const findOptions: SavedObjectsFindOptions = { + type: ['visualization'], + search: 'foo*', + perPage: 2, + }; + + const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const hits: SavedObjectsFindResult[] = []; + for await (const result of finder.find()) { + hits.push(...result.saved_objects); + await finder.close(); + } + + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledWith('test'); + }); + + test('causes generator to stop', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'test', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [mockHits[0]], + pit_id: 'test', + per_page: 1, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [mockHits[1]], + pit_id: 'test', + per_page: 1, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [], + per_page: 1, + pit_id: 'test', + page: 0, + }); + + const findOptions: SavedObjectsFindOptions = { + type: ['visualization'], + search: 'foo*', + perPage: 1, + }; + + const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const hits: SavedObjectsFindResult[] = []; + for await (const result of finder.find()) { + hits.push(...result.saved_objects); + await finder.close(); + } + + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); + expect(hits.length).toBe(1); + }); + + test('is called if `find` throws an error', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'test', + }); + savedObjectsClient.find.mockRejectedValueOnce(new Error('oops')); + + const findOptions: SavedObjectsFindOptions = { + type: ['visualization'], + search: 'foo*', + perPage: 2, + }; + + const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + const hits: SavedObjectsFindResult[] = []; + try { + for await (const result of finder.find()) { + hits.push(...result.saved_objects); + } + } catch (e) { + // intentionally empty + } + + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledWith('test'); + }); + + test('finder can be reused after closing', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'abc123', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: mockHits, + pit_id: 'abc123', + per_page: 1, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: mockHits, + pit_id: 'abc123', + per_page: 1, + page: 1, + }); + + const findOptions: SavedObjectsFindOptions = { + type: ['visualization'], + search: 'foo*', + perPage: 1, + }; + + const finder = createPointInTimeFinder({ findOptions, logger, savedObjectsClient }); + + const findA = finder.find(); + await findA.next(); + await finder.close(); + + const findB = finder.find(); + await findB.next(); + await finder.close(); + + expect((await findA.next()).done).toBe(true); + expect((await findB.next()).done).toBe(true); + expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/core/server/saved_objects/export/point_in_time_finder.ts b/src/core/server/saved_objects/export/point_in_time_finder.ts new file mode 100644 index 0000000000000..dc0bac6b6bfd9 --- /dev/null +++ b/src/core/server/saved_objects/export/point_in_time_finder.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Logger } from '../../logging'; +import { SavedObjectsClientContract, SavedObjectsFindOptions } from '../types'; +import { SavedObjectsFindResponse } from '../service'; + +/** + * Returns a generator to help page through large sets of saved objects. + * + * The generator wraps calls to `SavedObjects.find` and iterates over + * multiple pages of results using `_pit` and `search_after`. This will + * open a new Point In Time (PIT), and continue paging until a set of + * results is received that's smaller than the designated `perPage`. + * + * Once you have retrieved all of the results you need, it is recommended + * to call `close()` to clean up the PIT and prevent Elasticsearch from + * consuming resources unnecessarily. This will automatically be done for + * you if you reach the last page of results. + * + * @example + * ```ts + * const findOptions: SavedObjectsFindOptions = { + * type: 'visualization', + * search: 'foo*', + * perPage: 100, + * }; + * + * const finder = createPointInTimeFinder({ + * logger, + * savedObjectsClient, + * findOptions, + * }); + * + * const responses: SavedObjectFindResponse[] = []; + * for await (const response of finder.find()) { + * responses.push(...response); + * if (doneSearching) { + * await finder.close(); + * } + * } + * ``` + */ +export function createPointInTimeFinder({ + findOptions, + logger, + savedObjectsClient, +}: { + findOptions: SavedObjectsFindOptions; + logger: Logger; + savedObjectsClient: SavedObjectsClientContract; +}) { + return new PointInTimeFinder({ findOptions, logger, savedObjectsClient }); +} + +/** + * @internal + */ +export class PointInTimeFinder { + readonly #log: Logger; + readonly #savedObjectsClient: SavedObjectsClientContract; + readonly #findOptions: SavedObjectsFindOptions; + #open: boolean = false; + #pitId?: string; + + constructor({ + findOptions, + logger, + savedObjectsClient, + }: { + findOptions: SavedObjectsFindOptions; + logger: Logger; + savedObjectsClient: SavedObjectsClientContract; + }) { + this.#log = logger; + this.#savedObjectsClient = savedObjectsClient; + this.#findOptions = { + // Default to 1000 items per page as a tradeoff between + // speed and memory consumption. + perPage: 1000, + ...findOptions, + }; + } + + async *find() { + if (this.#open) { + throw new Error( + 'Point In Time has already been opened for this finder instance. ' + + 'Please call `close()` before calling `find()` again.' + ); + } + + // Open PIT and request our first page of hits + await this.open(); + + let lastResultsCount: number; + let lastHitSortValue: unknown[] | undefined; + do { + const results = await this.findNext({ + findOptions: this.#findOptions, + id: this.#pitId, + ...(lastHitSortValue ? { searchAfter: lastHitSortValue } : {}), + }); + this.#pitId = results.pit_id; + lastResultsCount = results.saved_objects.length; + lastHitSortValue = this.getLastHitSortValue(results); + + this.#log.debug(`Collected [${lastResultsCount}] saved objects for export.`); + + // Close PIT if this was our last page + if (this.#pitId && lastResultsCount < this.#findOptions.perPage!) { + await this.close(); + } + + yield results; + // We've reached the end when there are fewer hits than our perPage size, + // or when `close()` has been called. + } while (this.#open && lastResultsCount >= this.#findOptions.perPage!); + + return; + } + + async close() { + try { + if (this.#pitId) { + this.#log.debug(`Closing PIT for types [${this.#findOptions.type}]`); + await this.#savedObjectsClient.closePointInTime(this.#pitId); + this.#pitId = undefined; + } + this.#open = false; + } catch (e) { + this.#log.error(`Failed to close PIT for types [${this.#findOptions.type}]`); + throw e; + } + } + + private async open() { + try { + const { id } = await this.#savedObjectsClient.openPointInTimeForType(this.#findOptions.type); + this.#pitId = id; + this.#open = true; + } catch (e) { + // Since `find` swallows 404s, it is expected that exporter will do the same, + // so we only rethrow non-404 errors here. + if (e.output.statusCode !== 404) { + throw e; + } + this.#log.debug(`Unable to open PIT for types [${this.#findOptions.type}]: 404 ${e}`); + } + } + + private async findNext({ + findOptions, + id, + searchAfter, + }: { + findOptions: SavedObjectsFindOptions; + id?: string; + searchAfter?: unknown[]; + }) { + try { + return await this.#savedObjectsClient.find({ + // Sort fields are required to use searchAfter, so we set some defaults here + sortField: 'updated_at', + sortOrder: 'desc', + // Bump keep_alive by 2m on every new request to allow for the ES client + // to make multiple retries in the event of a network failure. + ...(id ? { pit: { id, keepAlive: '2m' } } : {}), + ...(searchAfter ? { searchAfter } : {}), + ...findOptions, + }); + } catch (e) { + if (id) { + // Clean up PIT on any errors. + await this.close(); + } + throw e; + } + } + + private getLastHitSortValue(res: SavedObjectsFindResponse): unknown[] | undefined { + if (res.saved_objects.length < 1) { + return undefined; + } + return res.saved_objects[res.saved_objects.length - 1].sort; + } +} diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index c16623f785b08..cf60ada5ba90a 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -11,6 +11,7 @@ import { SavedObjectsExporter } from './saved_objects_exporter'; import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { httpServerMock } from '../../http/http_server.mocks'; +import { loggerMock, MockedLogger } from '../../logging/logger.mock'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '@kbn/utils'; @@ -18,18 +19,25 @@ async function readStreamToCompletion(stream: Readable): Promise { + let logger: MockedLogger; let savedObjectsClient: ReturnType; let typeRegistry: SavedObjectTypeRegistry; let exporter: SavedObjectsExporter; beforeEach(() => { + logger = loggerMock.create(); typeRegistry = new SavedObjectTypeRegistry(); savedObjectsClient = savedObjectsClientMock.create(); - exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit, typeRegistry }); + exporter = new SavedObjectsExporter({ + exportSizeLimit, + logger, + savedObjectsClient, + typeRegistry, + }); }); describe('#exportByTypes', () => { @@ -58,7 +66,7 @@ describe('getSortedObjectsForExport()', () => { references: [], }, ], - per_page: 1, + per_page: 1000, page: 0, }); const exportStream = await exporter.exportByTypes({ @@ -96,30 +104,232 @@ describe('getSortedObjectsForExport()', () => { ] `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 1000, + "pit": Object { + "id": "some_pit_id", + "keepAlive": "2m", + }, + "search": undefined, + "sortField": "updated_at", + "sortOrder": "desc", + "type": Array [ + "index-pattern", + "search", ], - } + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); + }); + + describe('pages through results with PIT', () => { + function generateHits( + hitCount: number, + { + attributes = {}, + sort = [], + type = 'index-pattern', + }: { + attributes?: Record; + sort?: unknown[]; + type?: string; + } = {} + ) { + const hits = []; + for (let i = 1; i <= hitCount; i++) { + hits.push({ + id: `${i}`, + type, + attributes, + sort, + score: 1, + references: [], + }); + } + return hits; + } + + describe('<1k hits', () => { + const mockHits = generateHits(20); + + test('requests a single page', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 20, + saved_objects: mockHits, + per_page: 1000, + page: 0, + }); + + const exportStream = await exporter.exportByTypes({ + request, + types: ['index-pattern'], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(response[response.length - 1]).toMatchInlineSnapshot(` + Object { + "exportedCount": 20, + "missingRefCount": 0, + "missingReferences": Array [], + } + `); + }); + + test('opens and closes PIT', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 20, + saved_objects: mockHits, + per_page: 1000, + page: 0, + pit_id: 'abc123', + }); + + const exportStream = await exporter.exportByTypes({ + request, + types: ['index-pattern'], + }); + + await readStreamToCompletion(exportStream); + + expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); + }); + + test('passes correct PIT ID to `find`', async () => { + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'abc123', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 20, + saved_objects: mockHits, + per_page: 1000, + page: 0, + }); + + const exportStream = await exporter.exportByTypes({ + request, + types: ['index-pattern'], + }); + + await readStreamToCompletion(exportStream); + + expect(savedObjectsClient.find).toHaveBeenCalledWith( + expect.objectContaining({ + pit: expect.objectContaining({ id: 'abc123', keepAlive: '2m' }), + sortField: 'updated_at', + sortOrder: 'desc', + type: ['index-pattern'], + }) + ); + }); + }); + + describe('>1k hits', () => { + const firstMockHits = generateHits(1000, { sort: ['a', 'b'] }); + const secondMockHits = generateHits(500); + + test('requests multiple pages', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1500, + saved_objects: firstMockHits, + per_page: 1000, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1500, + saved_objects: secondMockHits, + per_page: 500, + page: 1, + }); + + const exportStream = await exporter.exportByTypes({ + request, + types: ['index-pattern'], + }); + + const response = await readStreamToCompletion(exportStream); + + expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); + expect(response[response.length - 1]).toMatchInlineSnapshot(` + Object { + "exportedCount": 1500, + "missingRefCount": 0, + "missingReferences": Array [], + } `); + }); + + test('opens and closes PIT', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1500, + saved_objects: firstMockHits, + per_page: 1000, + page: 0, + pit_id: 'abc123', + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1500, + saved_objects: secondMockHits, + per_page: 500, + page: 1, + pit_id: 'abc123', + }); + + const exportStream = await exporter.exportByTypes({ + request, + types: ['index-pattern'], + }); + + await readStreamToCompletion(exportStream); + + expect(savedObjectsClient.openPointInTimeForType).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); + }); + + test('passes sort values to searchAfter', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1500, + saved_objects: firstMockHits, + per_page: 1000, + page: 0, + }); + savedObjectsClient.find.mockResolvedValueOnce({ + total: 1500, + saved_objects: secondMockHits, + per_page: 500, + page: 1, + }); + + const exportStream = await exporter.exportByTypes({ + request, + types: ['index-pattern'], + }); + + await readStreamToCompletion(exportStream); + + expect(savedObjectsClient.find.mock.calls[1][0]).toEqual( + expect.objectContaining({ + searchAfter: ['a', 'b'], + }) + ); + }); + }); }); test('applies the export transforms', async () => { @@ -138,7 +348,12 @@ describe('getSortedObjectsForExport()', () => { }, }, }); - exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit, typeRegistry }); + exporter = new SavedObjectsExporter({ + exportSizeLimit, + logger, + savedObjectsClient, + typeRegistry, + }); savedObjectsClient.find.mockResolvedValueOnce({ total: 1, @@ -233,30 +448,36 @@ describe('getSortedObjectsForExport()', () => { ] `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 1000, + "pit": Object { + "id": "some_pit_id", + "keepAlive": "2m", + }, + "search": undefined, + "sortField": "updated_at", + "sortOrder": "desc", + "type": Array [ + "index-pattern", + "search", ], - } - `); + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('exclude export details if option is specified', async () => { @@ -383,30 +604,36 @@ describe('getSortedObjectsForExport()', () => { ] `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": undefined, - "perPage": 500, - "search": "foo", - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": undefined, + "perPage": 1000, + "pit": Object { + "id": "some_pit_id", + "keepAlive": "2m", + }, + "search": "foo", + "sortField": "updated_at", + "sortOrder": "desc", + "type": Array [ + "index-pattern", + "search", ], - } - `); + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('exports selected types with references when present', async () => { @@ -465,35 +692,41 @@ describe('getSortedObjectsForExport()', () => { ] `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": Array [ - Object { - "id": "1", - "type": "index-pattern", - }, - ], - "hasReferenceOperator": "OR", - "namespaces": undefined, - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], - ], - "results": Array [ + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": Array [ Object { - "type": "return", - "value": Promise {}, + "id": "1", + "type": "index-pattern", }, ], - } - `); + "hasReferenceOperator": "OR", + "namespaces": undefined, + "perPage": 1000, + "pit": Object { + "id": "some_pit_id", + "keepAlive": "2m", + }, + "search": undefined, + "sortField": "updated_at", + "sortOrder": "desc", + "type": Array [ + "index-pattern", + "search", + ], + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('exports from the provided namespace when present', async () => { @@ -521,7 +754,7 @@ describe('getSortedObjectsForExport()', () => { references: [], }, ], - per_page: 1, + per_page: 1000, page: 0, }); const exportStream = await exporter.exportByTypes({ @@ -560,36 +793,56 @@ describe('getSortedObjectsForExport()', () => { ] `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` - [MockFunction] { - "calls": Array [ - Array [ - Object { - "hasReference": undefined, - "hasReferenceOperator": undefined, - "namespaces": Array [ - "foo", - ], - "perPage": 500, - "search": undefined, - "type": Array [ - "index-pattern", - "search", - ], - }, - ], + [MockFunction] { + "calls": Array [ + Array [ + Object { + "hasReference": undefined, + "hasReferenceOperator": undefined, + "namespaces": Array [ + "foo", ], - "results": Array [ - Object { - "type": "return", - "value": Promise {}, - }, + "perPage": 1000, + "pit": Object { + "id": "some_pit_id", + "keepAlive": "2m", + }, + "search": undefined, + "sortField": "updated_at", + "sortOrder": "desc", + "type": Array [ + "index-pattern", + "search", ], - } - `); + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": Promise {}, + }, + ], + } + `); }); test('export selected types throws error when exceeding exportSizeLimit', async () => { - exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1, typeRegistry }); + exporter = new SavedObjectsExporter({ + exportSizeLimit: 1, + logger, + savedObjectsClient, + typeRegistry, + }); + + savedObjectsClient.openPointInTimeForType.mockResolvedValueOnce({ + id: 'abc123', + }); + + savedObjectsClient.closePointInTime.mockResolvedValueOnce({ + succeeded: true, + num_freed: 1, + }); savedObjectsClient.find.mockResolvedValueOnce({ total: 2, @@ -617,6 +870,7 @@ describe('getSortedObjectsForExport()', () => { ], per_page: 1, page: 0, + pit_id: 'abc123', }); await expect( exporter.exportByTypes({ @@ -624,12 +878,13 @@ describe('getSortedObjectsForExport()', () => { types: ['index-pattern', 'search'], }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Can't export more than 1 objects"`); + expect(savedObjectsClient.closePointInTime).toHaveBeenCalledTimes(1); }); test('sorts objects within type', async () => { savedObjectsClient.find.mockResolvedValueOnce({ total: 3, - per_page: 10000, + per_page: 1000, page: 1, saved_objects: [ { @@ -836,7 +1091,12 @@ describe('getSortedObjectsForExport()', () => { }); test('export selected objects throws error when exceeding exportSizeLimit', async () => { - exporter = new SavedObjectsExporter({ savedObjectsClient, exportSizeLimit: 1, typeRegistry }); + exporter = new SavedObjectsExporter({ + exportSizeLimit: 1, + logger, + savedObjectsClient, + typeRegistry, + }); const exportOpts = { request, diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index 295a3d7a119d4..c1c0ea73f0bd3 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -8,7 +8,9 @@ import { createListStream } from '@kbn/utils'; import { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObject, SavedObjectsClientContract } from '../types'; +import { Logger } from '../../logging'; +import { SavedObject, SavedObjectsClientContract, SavedObjectsFindOptions } from '../types'; +import { SavedObjectsFindResult } from '../service'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import { fetchNestedDependencies } from './fetch_nested_dependencies'; import { sortObjects } from './sort_objects'; @@ -21,6 +23,7 @@ import { } from './types'; import { SavedObjectsExportError } from './errors'; import { applyExportTransforms } from './apply_export_transforms'; +import { createPointInTimeFinder } from './point_in_time_finder'; import { byIdAscComparator, getPreservedOrderComparator, SavedObjectComparator } from './utils'; /** @@ -35,16 +38,20 @@ export class SavedObjectsExporter { readonly #savedObjectsClient: SavedObjectsClientContract; readonly #exportTransforms: Record; readonly #exportSizeLimit: number; + readonly #log: Logger; constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, + logger, }: { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; exportSizeLimit: number; + logger: Logger; }) { + this.#log = logger; this.#savedObjectsClient = savedObjectsClient; this.#exportSizeLimit = exportSizeLimit; this.#exportTransforms = typeRegistry.getAllTypes().reduce((transforms, type) => { @@ -66,6 +73,7 @@ export class SavedObjectsExporter { * @throws SavedObjectsExportError */ public async exportByTypes(options: SavedObjectsExportByTypeOptions) { + this.#log.debug(`Initiating export for types: [${options.types}]`); const objects = await this.fetchByTypes(options); return this.processObjects(objects, byIdAscComparator, { request: options.request, @@ -83,6 +91,7 @@ export class SavedObjectsExporter { * @throws SavedObjectsExportError */ public async exportByObjects(options: SavedObjectsExportByObjectOptions) { + this.#log.debug(`Initiating export of [${options.objects.length}] objects.`); if (options.objects.length > this.#exportSizeLimit) { throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit); } @@ -106,6 +115,7 @@ export class SavedObjectsExporter { namespace, }: SavedObjectExportBaseOptions ) { + this.#log.debug(`Processing [${savedObjects.length}] saved objects.`); let exportedObjects: Array>; let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; @@ -117,6 +127,7 @@ export class SavedObjectsExporter { }); if (includeReferencesDeep) { + this.#log.debug(`Fetching saved objects references.`); const fetchResult = await fetchNestedDependencies( savedObjects, this.#savedObjectsClient, @@ -138,6 +149,7 @@ export class SavedObjectsExporter { missingRefCount: missingReferences.length, missingReferences, }; + this.#log.debug(`Exporting [${redactedObjects.length}] saved objects.`); return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); } @@ -156,21 +168,32 @@ export class SavedObjectsExporter { hasReference, search, }: SavedObjectsExportByTypeOptions) { - const findResponse = await this.#savedObjectsClient.find({ + const findOptions: SavedObjectsFindOptions = { type: types, hasReference, hasReferenceOperator: hasReference ? 'OR' : undefined, search, - perPage: this.#exportSizeLimit, namespaces: namespace ? [namespace] : undefined, + }; + + const finder = createPointInTimeFinder({ + findOptions, + logger: this.#log, + savedObjectsClient: this.#savedObjectsClient, }); - if (findResponse.total > this.#exportSizeLimit) { - throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit); + + const hits: SavedObjectsFindResult[] = []; + for await (const result of finder.find()) { + hits.push(...result.saved_objects); + if (hits.length > this.#exportSizeLimit) { + await finder.close(); + throw SavedObjectsExportError.exportSizeExceeded(this.#exportSizeLimit); + } } // sorts server-side by _id, since it's only available in fielddata return ( - findResponse.saved_objects + hits // exclude the find-specific `score` property from the exported objects .map(({ score, ...obj }) => obj) .sort(byIdAscComparator) diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index 4ad0a34acc2ef..fce7f12384456 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -459,6 +459,7 @@ export class SavedObjectsService savedObjectsClient, typeRegistry: this.typeRegistry, exportSizeLimit: this.config!.maxImportExportSize, + logger: this.logger.get('exporter'), }), createImporter: (savedObjectsClient) => new SavedObjectsImporter({ diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index f6f8f88e84304..05a936db4bfee 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -9,7 +9,12 @@ // @ts-expect-error no ts import { esKuery } from '../../es_query'; -import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; +import { + validateFilterKueryNode, + validateConvertFilterToKueryNode, + fieldDefined, + hasFilterKeyError, +} from './filter_utils'; const mockMappings = { properties: { @@ -39,6 +44,18 @@ const mockMappings = { }, }, }, + bean: { + properties: { + canned: { + fields: { + text: { + type: 'text', + }, + }, + type: 'keyword', + }, + }, + }, alert: { properties: { actions: { @@ -90,6 +107,15 @@ describe('Filter Utils', () => { validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) ).toEqual(esKuery.fromKueryExpression('foo.title: "best"')); }); + test('Validate a multi-field KQL expression filter', () => { + expect( + validateConvertFilterToKueryNode( + ['bean'], + 'bean.attributes.canned.text: "best"', + mockMappings + ) + ).toEqual(esKuery.fromKueryExpression('bean.canned.text: "best"')); + }); test('Assemble filter kuery node saved object attributes with one saved object type', () => { expect( validateConvertFilterToKueryNode( @@ -485,4 +511,100 @@ describe('Filter Utils', () => { ]); }); }); + + describe('#hasFilterKeyError', () => { + test('Return no error if filter key is valid', () => { + const hasError = hasFilterKeyError('bean.attributes.canned.text', ['bean'], mockMappings); + + expect(hasError).toBeNull(); + }); + + test('Return error if key is not defined', () => { + const hasError = hasFilterKeyError(undefined, ['bean'], mockMappings); + + expect(hasError).toEqual( + 'The key is empty and needs to be wrapped by a saved object type like bean' + ); + }); + + test('Return error if key is null', () => { + const hasError = hasFilterKeyError(null, ['bean'], mockMappings); + + expect(hasError).toEqual( + 'The key is empty and needs to be wrapped by a saved object type like bean' + ); + }); + + test('Return error if key does not identify an SO wrapper', () => { + const hasError = hasFilterKeyError('beanattributescannedtext', ['bean'], mockMappings); + + expect(hasError).toEqual( + "This key 'beanattributescannedtext' need to be wrapped by a saved object type like bean" + ); + }); + + test('Return error if key does not match an SO type', () => { + const hasError = hasFilterKeyError('canned.attributes.bean.text', ['bean'], mockMappings); + + expect(hasError).toEqual('This type canned is not allowed'); + }); + + test('Return error if key does not match SO attribute structure', () => { + const hasError = hasFilterKeyError('bean.canned.text', ['bean'], mockMappings); + + expect(hasError).toEqual( + "This key 'bean.canned.text' does NOT match the filter proposition SavedObjectType.attributes.key" + ); + }); + + test('Return error if key matches SO attribute parent, not attribute itself', () => { + const hasError = hasFilterKeyError('alert.actions', ['alert'], mockMappings); + + expect(hasError).toEqual( + "This key 'alert.actions' does NOT match the filter proposition SavedObjectType.attributes.key" + ); + }); + + test('Return error if key refers to a non-existent attribute parent', () => { + const hasError = hasFilterKeyError('alert.not_a_key', ['alert'], mockMappings); + + expect(hasError).toEqual( + "This key 'alert.not_a_key' does NOT exist in alert saved object index patterns" + ); + }); + + test('Return error if key refers to a non-existent attribute', () => { + const hasError = hasFilterKeyError('bean.attributes.red', ['bean'], mockMappings); + + expect(hasError).toEqual( + "This key 'bean.attributes.red' does NOT exist in bean saved object index patterns" + ); + }); + }); + + describe('#fieldDefined', () => { + test('Return false if filter is using an non-existing key', () => { + const isFieldDefined = fieldDefined(mockMappings, 'foo.not_a_key'); + + expect(isFieldDefined).toBeFalsy(); + }); + + test('Return true if filter is using an existing key', () => { + const isFieldDefined = fieldDefined(mockMappings, 'foo.title'); + + expect(isFieldDefined).toBeTruthy(); + }); + + test('Return true if filter is using a default for a multi-field property', () => { + const isFieldDefined = fieldDefined(mockMappings, 'bean.canned'); + + expect(isFieldDefined).toBeTruthy(); + }); + + test('Return true if filter is using a non-default for a multi-field property', () => { + const isFieldDefined = fieldDefined(mockMappings, 'bean.canned.text'); + + expect(isFieldDefined).toBeTruthy(); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index b81c7d3e0885a..54b0033c9fcbe 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -205,7 +205,25 @@ export const hasFilterKeyError = ( return null; }; -const fieldDefined = (indexMappings: IndexMapping, key: string) => { +export const fieldDefined = (indexMappings: IndexMapping, key: string): boolean => { const mappingKey = 'properties.' + key.split('.').join('.properties.'); - return get(indexMappings, mappingKey) != null; + const potentialKey = get(indexMappings, mappingKey); + + // If the `mappingKey` does not match a valid path, before returning null, + // we want to check and see if the intended path was for a multi-field + // such as `x.attributes.field.text` where `field` is mapped to both text + // and keyword + if (potentialKey == null) { + const propertiesAttribute = 'properties'; + const indexOfLastProperties = mappingKey.lastIndexOf(propertiesAttribute); + const fieldMapping = mappingKey.substr(0, indexOfLastProperties); + const fieldType = mappingKey.substr( + mappingKey.lastIndexOf(propertiesAttribute) + `${propertiesAttribute}.`.length + ); + const mapping = `${fieldMapping}fields.${fieldType}`; + + return get(indexMappings, mapping) != null; + } else { + return true; + } }; diff --git a/src/core/server/saved_objects/service/lib/repository.mock.ts b/src/core/server/saved_objects/service/lib/repository.mock.ts index c853e208f27aa..a3610b1e437e2 100644 --- a/src/core/server/saved_objects/service/lib/repository.mock.ts +++ b/src/core/server/saved_objects/service/lib/repository.mock.ts @@ -17,6 +17,8 @@ const create = (): jest.Mocked => ({ bulkGet: jest.fn(), find: jest.fn(), get: jest.fn(), + closePointInTime: jest.fn(), + openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), addToNamespaces: jest.fn(), 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 aac508fb5b909..e77143d13612f 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -2813,6 +2813,13 @@ describe('SavedObjectsRepository', () => { expect(client.search).not.toHaveBeenCalled(); }); + it(`throws when a preference is provided with pit`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', pit: { id: 'abc123' }, preference: 'hi' }) + ).rejects.toThrowError('options.preference must be excluded when options.pit is used'); + expect(client.search).not.toHaveBeenCalled(); + }); + it(`throws when KQL filter syntax is invalid`, async () => { const findOpts = { namespaces: [namespace], @@ -2973,6 +2980,32 @@ describe('SavedObjectsRepository', () => { }); }); + it(`accepts searchAfter`, async () => { + const relevantOpts = { + ...commonOptions, + searchAfter: [1, 'a'], + }; + + await findSuccess(relevantOpts, namespace); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + searchAfter: [1, 'a'], + }); + }); + + it(`accepts pit`, async () => { + const relevantOpts = { + ...commonOptions, + pit: { id: 'abc123', keepAlive: '2m' }, + }; + + await findSuccess(relevantOpts, namespace); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + pit: { id: 'abc123', keepAlive: '2m' }, + }); + }); + it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => { const findOpts = { namespaces: [namespace], @@ -4393,4 +4426,136 @@ describe('SavedObjectsRepository', () => { }); }); }); + + describe('#openPointInTimeForType', () => { + const type = 'index-pattern'; + + const generateResults = (id) => ({ id: id || null }); + const successResponse = async (type, options) => { + client.openPointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults()) + ); + const result = await savedObjectsRepository.openPointInTimeForType(type, options); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + return result; + }; + + describe('client calls', () => { + it(`should use the ES PIT API`, async () => { + await successResponse(type); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + }); + + it(`accepts preference`, async () => { + await successResponse(type, { preference: 'pref' }); + expect(client.openPointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + preference: 'pref', + }), + expect.anything() + ); + }); + + it(`accepts keepAlive`, async () => { + await successResponse(type, { keepAlive: '2m' }); + expect(client.openPointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + keep_alive: '2m', + }), + expect.anything() + ); + }); + + it(`defaults keepAlive to 5m`, async () => { + await successResponse(type); + expect(client.openPointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + keep_alive: '5m', + }), + expect.anything() + ); + }); + }); + + describe('errors', () => { + const expectNotFoundError = async (types) => { + await expect(savedObjectsRepository.openPointInTimeForType(types)).rejects.toThrowError( + createGenericNotFoundError() + ); + }; + + it(`throws when ES is unable to find the index`, async () => { + client.openPointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + await expectNotFoundError(type); + expect(client.openPointInTime).toHaveBeenCalledTimes(1); + }); + + it(`should return generic not found error when attempting to find only invalid or hidden types`, async () => { + const test = async (types) => { + await expectNotFoundError(types); + expect(client.openPointInTime).not.toHaveBeenCalled(); + }; + + await test('unknownType'); + await test(HIDDEN_TYPE); + await test(['unknownType', HIDDEN_TYPE]); + }); + }); + + describe('returns', () => { + it(`returns id in the expected format`, async () => { + const id = 'abc123'; + const results = generateResults(id); + client.openPointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(results) + ); + const response = await savedObjectsRepository.openPointInTimeForType(type); + expect(response).toEqual({ id }); + }); + }); + }); + + describe('#closePointInTime', () => { + const generateResults = () => ({ succeeded: true, num_freed: 3 }); + const successResponse = async (id) => { + client.closePointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(generateResults()) + ); + const result = await savedObjectsRepository.closePointInTime(id); + expect(client.closePointInTime).toHaveBeenCalledTimes(1); + return result; + }; + + describe('client calls', () => { + it(`should use the ES PIT API`, async () => { + await successResponse('abc123'); + expect(client.closePointInTime).toHaveBeenCalledTimes(1); + }); + + it(`accepts id`, async () => { + await successResponse('abc123'); + expect(client.closePointInTime).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + id: 'abc123', + }), + }), + expect.anything() + ); + }); + }); + + describe('returns', () => { + it(`returns response body from ES`, async () => { + const results = generateResults('abc123'); + client.closePointInTime.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise(results) + ); + const response = await savedObjectsRepository.closePointInTime('abc123'); + expect(response).toEqual(results); + }); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index fcd72aa4326a2..b8a72377b0d76 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -36,6 +36,10 @@ import { SavedObjectsCreateOptions, SavedObjectsFindResponse, SavedObjectsFindResult, + SavedObjectsClosePointInTimeOptions, + SavedObjectsClosePointInTimeResponse, + SavedObjectsOpenPointInTimeOptions, + SavedObjectsOpenPointInTimeResponse, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, SavedObjectsBulkUpdateObject, @@ -708,11 +712,13 @@ export class SavedObjectsRepository { * Query field argument for more information * @property {integer} [options.page=1] * @property {integer} [options.perPage=20] + * @property {Array} [options.searchAfter] * @property {string} [options.sortField] * @property {string} [options.sortOrder] * @property {Array} [options.fields] * @property {string} [options.namespace] * @property {object} [options.hasReference] - { type, id } + * @property {string} [options.pit] * @property {string} [options.preference] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ @@ -726,6 +732,8 @@ export class SavedObjectsRepository { hasReferenceOperator, page = FIND_DEFAULT_PAGE, perPage = FIND_DEFAULT_PER_PAGE, + pit, + searchAfter, sortField, sortOrder, fields, @@ -752,6 +760,10 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createBadRequestError( 'options.namespaces must be an empty array when options.typeToNamespacesMap is used' ); + } else if (preference?.length && pit) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.preference must be excluded when options.pit is used' + ); } const types = type @@ -787,20 +799,24 @@ export class SavedObjectsRepository { } const esOptions = { - index: this.getIndicesForTypes(allowedTypes), - size: perPage, - from: perPage * (page - 1), + // If `pit` is provided, we drop the `index`, otherwise ES returns 400. + ...(pit ? {} : { index: this.getIndicesForTypes(allowedTypes) }), + // If `searchAfter` is provided, we drop `from` as it will not be used for pagination. + ...(searchAfter ? {} : { from: perPage * (page - 1) }), _source: includedFields(type, fields), - rest_total_hits_as_int: true, preference, + rest_total_hits_as_int: true, + size: perPage, body: { seq_no_primary_term: true, ...getSearchDsl(this._mappings, this._registry, { search, defaultSearchOperator, searchFields, + pit, rootSearchFields, type: allowedTypes, + searchAfter, sortField, sortOrder, namespaces, @@ -834,8 +850,10 @@ export class SavedObjectsRepository { (hit: SavedObjectsRawDoc): SavedObjectsFindResult => ({ ...this._rawToSavedObject(hit), score: (hit as any)._score, + ...((hit as any).sort && { sort: (hit as any).sort }), }) ), + ...(body.pit_id && { pit_id: body.pit_id }), } as SavedObjectsFindResponse; } @@ -1764,6 +1782,118 @@ export class SavedObjectsRepository { }; } + /** + * Opens a Point In Time (PIT) against the indices for the specified Saved Object types. + * The returned `id` can then be passed to `SavedObjects.find` to search against that PIT. + * + * @example + * ```ts + * const { id } = await savedObjectsClient.openPointInTimeForType( + * type: 'visualization', + * { keepAlive: '5m' }, + * ); + * const page1 = await savedObjectsClient.find({ + * type: 'visualization', + * sortField: 'updated_at', + * sortOrder: 'asc', + * pit: { id, keepAlive: '2m' }, + * }); + * const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; + * const page2 = await savedObjectsClient.find({ + * type: 'visualization', + * sortField: 'updated_at', + * sortOrder: 'asc', + * pit: { id: page1.pit_id }, + * searchAfter: lastHit.sort, + * }); + * await savedObjectsClient.closePointInTime(page2.pit_id); + * ``` + * + * @param {string|Array} type + * @param {object} [options] - {@link SavedObjectsOpenPointInTimeOptions} + * @property {string} [options.keepAlive] + * @property {string} [options.preference] + * @returns {promise} - { id: string } + */ + async openPointInTimeForType( + type: string | string[], + { keepAlive = '5m', preference }: SavedObjectsOpenPointInTimeOptions = {} + ): Promise { + const types = Array.isArray(type) ? type : [type]; + const allowedTypes = types.filter((t) => this._allowedTypes.includes(t)); + if (allowedTypes.length === 0) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + } + + const esOptions = { + index: this.getIndicesForTypes(allowedTypes), + keep_alive: keepAlive, + ...(preference ? { preference } : {}), + }; + + const { + body, + statusCode, + } = await this.client.openPointInTime(esOptions, { + ignore: [404], + }); + if (statusCode === 404) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + } + + return { + id: body.id, + }; + } + + /** + * Closes a Point In Time (PIT) by ID. This simply proxies the request to ES + * via the Elasticsearch client, and is included in the Saved Objects Client + * as a convenience for consumers who are using `openPointInTimeForType`. + * + * @remarks + * While the `keepAlive` that is provided will cause a PIT to automatically close, + * it is highly recommended to explicitly close a PIT when you are done with it + * in order to avoid consuming unneeded resources in Elasticsearch. + * + * @example + * ```ts + * const repository = coreStart.savedObjects.createInternalRepository(); + * + * const { id } = await repository.openPointInTimeForType( + * type: 'index-pattern', + * { keepAlive: '2m' }, + * ); + * + * const response = await repository.find({ + * type: 'index-pattern', + * search: 'foo*', + * sortField: 'name', + * sortOrder: 'desc', + * pit: { + * id: 'abc123', + * keepAlive: '2m', + * }, + * searchAfter: [1234, 'abcd'], + * }); + * + * await repository.closePointInTime(response.pit_id); + * ``` + * + * @param {string} id + * @param {object} [options] - {@link SavedObjectsClosePointInTimeOptions} + * @returns {promise} - {@link SavedObjectsClosePointInTimeResponse} + */ + async closePointInTime( + id: string, + options?: SavedObjectsClosePointInTimeOptions + ): Promise { + const { body } = await this.client.closePointInTime({ + body: { id }, + }); + return body; + } + /** * Returns index specified by the given type or the default index * diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.ts b/src/core/server/saved_objects/service/lib/repository_es_client.ts index dae72819726ad..6a601b1ed0c83 100644 --- a/src/core/server/saved_objects/service/lib/repository_es_client.ts +++ b/src/core/server/saved_objects/service/lib/repository_es_client.ts @@ -14,11 +14,13 @@ import { decorateEsError } from './decorate_es_error'; const methods = [ 'bulk', + 'closePointInTime', 'create', 'delete', 'get', 'index', 'mget', + 'openPointInTime', 'search', 'update', 'updateByQuery', diff --git a/src/core/server/saved_objects/service/lib/search_dsl/pit_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.test.ts new file mode 100644 index 0000000000000..5a99168792e83 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getPitParams } from './pit_params'; + +describe('searchDsl/getPitParams', () => { + it('returns only an ID by default', () => { + expect(getPitParams({ id: 'abc123' })).toEqual({ + pit: { + id: 'abc123', + }, + }); + }); + + it('includes keepAlive if provided and rewrites to snake case', () => { + expect(getPitParams({ id: 'abc123', keepAlive: '2m' })).toEqual({ + pit: { + id: 'abc123', + keep_alive: '2m', + }, + }); + }); +}); diff --git a/src/type_definitions/react_virtualized.d.ts b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.ts similarity index 60% rename from src/type_definitions/react_virtualized.d.ts rename to src/core/server/saved_objects/service/lib/search_dsl/pit_params.ts index d78a159b71560..1a8dcb5cca2e9 100644 --- a/src/type_definitions/react_virtualized.d.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/pit_params.ts @@ -6,6 +6,13 @@ * Side Public License, v 1. */ -declare module 'react-virtualized' { - export type ListProps = any; +import { SavedObjectsPitParams } from '../../../types'; + +export function getPitParams(pit: SavedObjectsPitParams) { + return { + pit: { + id: pit.id, + ...(pit.keepAlive ? { keep_alive: pit.keepAlive } : {}), + }, + }; } 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 9e91e585f74f0..fc26c837d5e52 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 @@ -6,14 +6,17 @@ * Side Public License, v 1. */ +jest.mock('./pit_params'); jest.mock('./query_params'); jest.mock('./sorting_params'); import { typeRegistryMock } from '../../../saved_objects_type_registry.mock'; +import * as pitParamsNS from './pit_params'; import * as queryParamsNS from './query_params'; import { getSearchDsl } from './search_dsl'; import * as sortParamsNS from './sorting_params'; +const getPitParams = pitParamsNS.getPitParams as jest.Mock; const getQueryParams = queryParamsNS.getQueryParams as jest.Mock; const getSortingParams = sortParamsNS.getSortingParams as jest.Mock; @@ -84,6 +87,7 @@ describe('getSearchDsl', () => { type: 'foo', sortField: 'bar', sortOrder: 'baz', + pit: { id: 'abc123' }, }; getSearchDsl(mappings, registry, opts); @@ -92,7 +96,8 @@ describe('getSearchDsl', () => { mappings, opts.type, opts.sortField, - opts.sortOrder + opts.sortOrder, + opts.pit ); }); @@ -101,5 +106,33 @@ describe('getSearchDsl', () => { getSortingParams.mockReturnValue({ b: 'b' }); expect(getSearchDsl(mappings, registry, { type: 'foo' })).toEqual({ a: 'a', b: 'b' }); }); + + it('returns searchAfter if provided', () => { + getQueryParams.mockReturnValue({ a: 'a' }); + getSortingParams.mockReturnValue({ b: 'b' }); + expect(getSearchDsl(mappings, registry, { type: 'foo', searchAfter: [1, 'bar'] })).toEqual({ + a: 'a', + b: 'b', + search_after: [1, 'bar'], + }); + }); + + it('returns pit if provided', () => { + getQueryParams.mockReturnValue({ a: 'a' }); + getSortingParams.mockReturnValue({ b: 'b' }); + getPitParams.mockReturnValue({ pit: { id: 'abc123' } }); + expect( + getSearchDsl(mappings, registry, { + type: 'foo', + searchAfter: [1, 'bar'], + pit: { id: 'abc123' }, + }) + ).toEqual({ + a: 'a', + b: 'b', + pit: { id: 'abc123' }, + search_after: [1, 'bar'], + }); + }); }); }); 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 4b4fa8865ee9d..cae5e43897bcf 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 @@ -9,7 +9,9 @@ import Boom from '@hapi/boom'; import { IndexMapping } from '../../../mappings'; +import { SavedObjectsPitParams } from '../../../types'; import { getQueryParams, HasReferenceQueryParams, SearchOperator } from './query_params'; +import { getPitParams } from './pit_params'; import { getSortingParams } from './sorting_params'; import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; @@ -21,9 +23,11 @@ interface GetSearchDslOptions { defaultSearchOperator?: SearchOperator; searchFields?: string[]; rootSearchFields?: string[]; + searchAfter?: unknown[]; sortField?: string; sortOrder?: string; namespaces?: string[]; + pit?: SavedObjectsPitParams; typeToNamespacesMap?: Map; hasReference?: HasReferenceQueryParams | HasReferenceQueryParams[]; hasReferenceOperator?: SearchOperator; @@ -41,9 +45,11 @@ export function getSearchDsl( defaultSearchOperator, searchFields, rootSearchFields, + searchAfter, sortField, sortOrder, namespaces, + pit, typeToNamespacesMap, hasReference, hasReferenceOperator, @@ -72,6 +78,8 @@ export function getSearchDsl( hasReferenceOperator, kueryNode, }), - ...getSortingParams(mappings, type, sortField, sortOrder), + ...getSortingParams(mappings, type, sortField, sortOrder, pit), + ...(pit ? getPitParams(pit) : {}), + ...(searchAfter ? { search_after: searchAfter } : {}), }; } diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts index 1376f0d50a9da..73c7065705fc5 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.test.ts @@ -79,6 +79,11 @@ describe('searchDsl/getSortParams', () => { ], }); }); + it('appends tiebreaker when PIT is provided', () => { + expect(getSortingParams(MAPPINGS, 'saved', 'title', undefined, { id: 'abc' }).sort).toEqual( + expect.arrayContaining([{ _shard_doc: 'asc' }]) + ); + }); }); describe('sortField is simple root property with multiple types', () => { it('returns correct params', () => { @@ -93,6 +98,11 @@ describe('searchDsl/getSortParams', () => { ], }); }); + it('appends tiebreaker when PIT is provided', () => { + expect( + getSortingParams(MAPPINGS, ['saved', 'pending'], 'type', undefined, { id: 'abc' }).sort + ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); + }); }); describe('sortField is simple non-root property with multiple types', () => { it('returns correct params', () => { @@ -114,6 +124,11 @@ describe('searchDsl/getSortParams', () => { ], }); }); + it('appends tiebreaker when PIT is provided', () => { + expect( + getSortingParams(MAPPINGS, 'saved', 'title.raw', undefined, { id: 'abc' }).sort + ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); + }); }); describe('sortField is multi-field with single type as array', () => { it('returns correct params', () => { @@ -128,6 +143,11 @@ describe('searchDsl/getSortParams', () => { ], }); }); + it('appends tiebreaker when PIT is provided', () => { + expect( + getSortingParams(MAPPINGS, ['saved'], 'title.raw', undefined, { id: 'abc' }).sort + ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); + }); }); describe('sortField is root multi-field with multiple types', () => { it('returns correct params', () => { @@ -142,6 +162,12 @@ describe('searchDsl/getSortParams', () => { ], }); }); + it('appends tiebreaker when PIT is provided', () => { + expect( + getSortingParams(MAPPINGS, ['saved', 'pending'], 'type.raw', undefined, { id: 'abc' }) + .sort + ).toEqual(expect.arrayContaining([{ _shard_doc: 'asc' }])); + }); }); describe('sortField is not-root multi-field with multiple types', () => { it('returns correct params', () => { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts index e3bfba6a80f59..abef9bfa0a300 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/sorting_params.ts @@ -8,6 +8,12 @@ import Boom from '@hapi/boom'; import { getProperty, IndexMapping } from '../../../mappings'; +import { SavedObjectsPitParams } from '../../../types'; + +// TODO: The plan is for ES to automatically add this tiebreaker when +// using PIT. We should remove this logic once that is resolved. +// https://github.com/elastic/elasticsearch/issues/56828 +const ES_PROVIDED_TIEBREAKER = { _shard_doc: 'asc' }; const TOP_LEVEL_FIELDS = ['_id', '_score']; @@ -15,7 +21,8 @@ export function getSortingParams( mappings: IndexMapping, type: string | string[], sortField?: string, - sortOrder?: string + sortOrder?: string, + pit?: SavedObjectsPitParams ) { if (!sortField) { return {}; @@ -31,6 +38,7 @@ export function getSortingParams( order: sortOrder, }, }, + ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } @@ -51,6 +59,7 @@ export function getSortingParams( unmapped_type: rootField.type, }, }, + ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } @@ -75,6 +84,7 @@ export function getSortingParams( unmapped_type: field.type, }, }, + ...(pit ? [ES_PROVIDED_TIEBREAKER] : []), ], }; } diff --git a/src/core/server/saved_objects/service/saved_objects_client.mock.ts b/src/core/server/saved_objects/service/saved_objects_client.mock.ts index 72f5561aa7027..ecca652cace37 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.mock.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.mock.ts @@ -20,6 +20,8 @@ const create = () => bulkGet: jest.fn(), find: jest.fn(), get: jest.fn(), + closePointInTime: jest.fn(), + openPointInTimeForType: jest.fn().mockResolvedValue({ id: 'some_pit_id' }), resolve: jest.fn(), update: jest.fn(), addToNamespaces: jest.fn(), diff --git a/src/core/server/saved_objects/service/saved_objects_client.test.js b/src/core/server/saved_objects/service/saved_objects_client.test.js index 45b0cf70b0dc6..7cbddaf195dc9 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.test.js +++ b/src/core/server/saved_objects/service/saved_objects_client.test.js @@ -115,6 +115,36 @@ test(`#get`, async () => { expect(result).toBe(returnValue); }); +test(`#openPointInTimeForType`, async () => { + const returnValue = Symbol(); + const mockRepository = { + openPointInTimeForType: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const type = Symbol(); + const options = Symbol(); + const result = await client.openPointInTimeForType(type, options); + + expect(mockRepository.openPointInTimeForType).toHaveBeenCalledWith(type, options); + expect(result).toBe(returnValue); +}); + +test(`#closePointInTime`, async () => { + const returnValue = Symbol(); + const mockRepository = { + closePointInTime: jest.fn().mockResolvedValue(returnValue), + }; + const client = new SavedObjectsClient(mockRepository); + + const id = Symbol(); + const options = Symbol(); + const result = await client.closePointInTime(id, options); + + expect(mockRepository.closePointInTime).toHaveBeenCalledWith(id, options); + expect(result).toBe(returnValue); +}); + test(`#resolve`, async () => { const returnValue = Symbol(); const mockRepository = { diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index b90540fbfa971..b93f3022e4236 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -129,6 +129,35 @@ export interface SavedObjectsFindResult extends SavedObject { * The Elasticsearch `_score` of this result. */ score: number; + /** + * The Elasticsearch `sort` value of this result. + * + * @remarks + * This can be passed directly to the `searchAfter` param in the {@link SavedObjectsFindOptions} + * in order to page through large numbers of hits. It is recommended you use this alongside + * a Point In Time (PIT) that was opened with {@link SavedObjectsClient.openPointInTimeForType}. + * + * @example + * ```ts + * const { id } = await savedObjectsClient.openPointInTimeForType('visualization'); + * const page1 = await savedObjectsClient.find({ + * type: 'visualization', + * sortField: 'updated_at', + * sortOrder: 'asc', + * pit: { id }, + * }); + * const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; + * const page2 = await savedObjectsClient.find({ + * type: 'visualization', + * sortField: 'updated_at', + * sortOrder: 'asc', + * pit: { id: page1.pit_id }, + * searchAfter: lastHit.sort, + * }); + * await savedObjectsClient.closePointInTime(page2.pit_id); + * ``` + */ + sort?: unknown[]; } /** @@ -144,6 +173,7 @@ export interface SavedObjectsFindResponse { total: number; per_page: number; page: number; + pit_id?: string; } /** @@ -311,6 +341,50 @@ export interface SavedObjectsResolveResponse { outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; } +/** + * @public + */ +export interface SavedObjectsOpenPointInTimeOptions extends SavedObjectsBaseOptions { + /** + * Optionally specify how long ES should keep the PIT alive until the next request. Defaults to `5m`. + */ + keepAlive?: string; + /** + * An optional ES preference value to be used for the query. + */ + preference?: string; +} + +/** + * @public + */ +export interface SavedObjectsOpenPointInTimeResponse { + /** + * PIT ID returned from ES. + */ + id: string; +} + +/** + * @public + */ +export type SavedObjectsClosePointInTimeOptions = SavedObjectsBaseOptions; + +/** + * @public + */ +export interface SavedObjectsClosePointInTimeResponse { + /** + * If true, all search contexts associated with the PIT id are + * successfully closed. + */ + succeeded: boolean; + /** + * The number of search contexts that have been successfully closed. + */ + num_freed: number; +} + /** * * @public @@ -504,4 +578,25 @@ export class SavedObjectsClient { ) { return await this._repository.removeReferencesTo(type, id, options); } + + /** + * Opens a Point In Time (PIT) against the indices for the specified Saved Object types. + * The returned `id` can then be passed to {@link SavedObjectsClient.find} to search + * against that PIT. + */ + async openPointInTimeForType( + type: string | string[], + options: SavedObjectsOpenPointInTimeOptions = {} + ) { + return await this._repository.openPointInTimeForType(type, options); + } + + /** + * Closes a Point In Time (PIT) by ID. This simply proxies the request to ES via the + * Elasticsearch client, and is included in the Saved Objects Client as a convenience + * for consumers who are using {@link SavedObjectsClient.openPointInTimeForType}. + */ + async closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions) { + return await this._repository.closePointInTime(id, options); + } } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index d122e92aba398..66110d096213f 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -62,6 +62,14 @@ export interface SavedObjectsFindOptionsReference { id: string; } +/** + * @public + */ +export interface SavedObjectsPitParams { + id: string; + keepAlive?: string; +} + /** * * @public @@ -82,6 +90,10 @@ export interface SavedObjectsFindOptions { search?: string; /** The fields to perform the parsed query against. See Elasticsearch Simple Query String `fields` argument for more information */ searchFields?: string[]; + /** + * Use the sort values from the previous page to retrieve the next page of results. + */ + searchAfter?: unknown[]; /** * The fields to perform the parsed query against. Unlike the `searchFields` argument, these are expected to be root fields and will not * be modified. If used in conjunction with `searchFields`, both are concatenated together. @@ -114,6 +126,10 @@ export interface SavedObjectsFindOptions { typeToNamespacesMap?: Map; /** An optional ES preference value to be used for the query **/ preference?: string; + /** + * Search against a specific Point In Time (PIT) that you've opened with {@link SavedObjectsClient.openPointInTimeForType}. + */ + pit?: SavedObjectsPitParams; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 2b36ab58896b1..b5f8b9d69abf3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -449,6 +449,7 @@ export interface CoreConfigUsageData { }; // (undocumented) savedObjects: { + customIndex: boolean; maxImportPayloadBytes: number; maxImportExportSizeBytes: number; }; @@ -2222,6 +2223,7 @@ export class SavedObjectsClient { bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; + closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; @@ -2231,6 +2233,7 @@ export class SavedObjectsClient { errors: typeof SavedObjectsErrorHelpers; find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; + openPointInTimeForType(type: string | string[], options?: SavedObjectsOpenPointInTimeOptions): Promise; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; @@ -2269,6 +2272,15 @@ export interface SavedObjectsClientWrapperOptions { typeRegistry: ISavedObjectTypeRegistry; } +// @public (undocumented) +export type SavedObjectsClosePointInTimeOptions = SavedObjectsBaseOptions; + +// @public (undocumented) +export interface SavedObjectsClosePointInTimeResponse { + num_freed: number; + succeeded: boolean; +} + // @public export interface SavedObjectsComplexFieldMapping { // (undocumented) @@ -2419,10 +2431,11 @@ export interface SavedObjectsExportByTypeOptions extends SavedObjectExportBaseOp export class SavedObjectsExporter { // (undocumented) #private; - constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, }: { + constructor({ savedObjectsClient, typeRegistry, exportSizeLimit, logger, }: { savedObjectsClient: SavedObjectsClientContract; typeRegistry: ISavedObjectTypeRegistry; exportSizeLimit: number; + logger: Logger; }); exportByObjects(options: SavedObjectsExportByObjectOptions): Promise; exportByTypes(options: SavedObjectsExportByTypeOptions): Promise; @@ -2480,9 +2493,11 @@ export interface SavedObjectsFindOptions { page?: number; // (undocumented) perPage?: number; + pit?: SavedObjectsPitParams; preference?: string; rootSearchFields?: string[]; search?: string; + searchAfter?: unknown[]; searchFields?: string[]; // (undocumented) sortField?: string; @@ -2508,6 +2523,8 @@ export interface SavedObjectsFindResponse { // (undocumented) per_page: number; // (undocumented) + pit_id?: string; + // (undocumented) saved_objects: Array>; // (undocumented) total: number; @@ -2516,6 +2533,7 @@ export interface SavedObjectsFindResponse { // @public (undocumented) export interface SavedObjectsFindResult extends SavedObject { score: number; + sort?: unknown[]; } // @public @@ -2742,6 +2760,25 @@ export interface SavedObjectsMigrationVersion { // @public export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'agnostic'; +// @public (undocumented) +export interface SavedObjectsOpenPointInTimeOptions extends SavedObjectsBaseOptions { + keepAlive?: string; + preference?: string; +} + +// @public (undocumented) +export interface SavedObjectsOpenPointInTimeResponse { + id: string; +} + +// @public (undocumented) +export interface SavedObjectsPitParams { + // (undocumented) + id: string; + // (undocumented) + keepAlive?: string; +} + // @public export interface SavedObjectsRawDoc { // (undocumented) @@ -2778,6 +2815,7 @@ export class SavedObjectsRepository { bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; + closePointInTime(id: string, options?: SavedObjectsClosePointInTimeOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts // @@ -2790,6 +2828,7 @@ export class SavedObjectsRepository { find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFields: Array, options?: SavedObjectsIncrementCounterOptions): Promise>; + openPointInTimeForType(type: string | string[], { keepAlive, preference }?: SavedObjectsOpenPointInTimeOptions): Promise; removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise; resolve(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; @@ -2954,10 +2993,12 @@ export interface SearchResponse { highlight?: any; inner_hits?: any; matched_queries?: string[]; - sort?: string[]; + sort?: unknown[]; }>; }; // (undocumented) + pit_id?: string; + // (undocumented) _scroll_id?: string; // (undocumented) _shards: ShardsResponse; diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 1839ee68190aa..2ae51d4452a4e 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -31,6 +31,7 @@ export type { SavedObjectStatusMeta, SavedObjectsFindOptionsReference, SavedObjectsFindOptions, + SavedObjectsPitParams, SavedObjectsBaseOptions, MutatingOperationRefreshSetting, SavedObjectsClientContract, diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 1460520833460..2c9dfbe6fcc10 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -124,7 +124,6 @@ export const REMOVE_EXTENSION = ['packages/kbn-plugin-generator/template/**/*.ej * @type {Array} */ export const TEMPORARILY_IGNORED_PATHS = [ - 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', 'src/core/server/core_app/assets/favicons/android-chrome-256x256.png', 'src/core/server/core_app/assets/favicons/android-chrome-512x512.png', diff --git a/src/fixtures/agg_resp/date_histogram.js b/src/fixtures/agg_resp/date_histogram.js deleted file mode 100644 index 29b34f1ce69d0..0000000000000 --- a/src/fixtures/agg_resp/date_histogram.js +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export default { - took: 35, - timed_out: false, - _shards: { - total: 2, - successful: 2, - failed: 0, - }, - hits: { - total: 32899, - max_score: 0, - hits: [], - }, - aggregations: { - 1: { - buckets: [ - { - key_as_string: '2015-01-30T01:00:00.000Z', - key: 1422579600000, - doc_count: 18, - }, - { - key_as_string: '2015-01-30T02:00:00.000Z', - key: 1422583200000, - doc_count: 68, - }, - { - key_as_string: '2015-01-30T03:00:00.000Z', - key: 1422586800000, - doc_count: 146, - }, - { - key_as_string: '2015-01-30T04:00:00.000Z', - key: 1422590400000, - doc_count: 149, - }, - { - key_as_string: '2015-01-30T05:00:00.000Z', - key: 1422594000000, - doc_count: 363, - }, - { - key_as_string: '2015-01-30T06:00:00.000Z', - key: 1422597600000, - doc_count: 555, - }, - { - key_as_string: '2015-01-30T07:00:00.000Z', - key: 1422601200000, - doc_count: 878, - }, - { - key_as_string: '2015-01-30T08:00:00.000Z', - key: 1422604800000, - doc_count: 1133, - }, - { - key_as_string: '2015-01-30T09:00:00.000Z', - key: 1422608400000, - doc_count: 1438, - }, - { - key_as_string: '2015-01-30T10:00:00.000Z', - key: 1422612000000, - doc_count: 1719, - }, - { - key_as_string: '2015-01-30T11:00:00.000Z', - key: 1422615600000, - doc_count: 1813, - }, - { - key_as_string: '2015-01-30T12:00:00.000Z', - key: 1422619200000, - doc_count: 1790, - }, - { - key_as_string: '2015-01-30T13:00:00.000Z', - key: 1422622800000, - doc_count: 1582, - }, - { - key_as_string: '2015-01-30T14:00:00.000Z', - key: 1422626400000, - doc_count: 1439, - }, - { - key_as_string: '2015-01-30T15:00:00.000Z', - key: 1422630000000, - doc_count: 1154, - }, - { - key_as_string: '2015-01-30T16:00:00.000Z', - key: 1422633600000, - doc_count: 847, - }, - { - key_as_string: '2015-01-30T17:00:00.000Z', - key: 1422637200000, - doc_count: 588, - }, - { - key_as_string: '2015-01-30T18:00:00.000Z', - key: 1422640800000, - doc_count: 374, - }, - { - key_as_string: '2015-01-30T19:00:00.000Z', - key: 1422644400000, - doc_count: 152, - }, - { - key_as_string: '2015-01-30T20:00:00.000Z', - key: 1422648000000, - doc_count: 140, - }, - { - key_as_string: '2015-01-30T21:00:00.000Z', - key: 1422651600000, - doc_count: 73, - }, - { - key_as_string: '2015-01-30T22:00:00.000Z', - key: 1422655200000, - doc_count: 28, - }, - { - key_as_string: '2015-01-30T23:00:00.000Z', - key: 1422658800000, - doc_count: 9, - }, - { - key_as_string: '2015-01-31T00:00:00.000Z', - key: 1422662400000, - doc_count: 29, - }, - { - key_as_string: '2015-01-31T01:00:00.000Z', - key: 1422666000000, - doc_count: 38, - }, - { - key_as_string: '2015-01-31T02:00:00.000Z', - key: 1422669600000, - doc_count: 70, - }, - { - key_as_string: '2015-01-31T03:00:00.000Z', - key: 1422673200000, - doc_count: 136, - }, - { - key_as_string: '2015-01-31T04:00:00.000Z', - key: 1422676800000, - doc_count: 173, - }, - { - key_as_string: '2015-01-31T05:00:00.000Z', - key: 1422680400000, - doc_count: 370, - }, - { - key_as_string: '2015-01-31T06:00:00.000Z', - key: 1422684000000, - doc_count: 545, - }, - { - key_as_string: '2015-01-31T07:00:00.000Z', - key: 1422687600000, - doc_count: 845, - }, - { - key_as_string: '2015-01-31T08:00:00.000Z', - key: 1422691200000, - doc_count: 1070, - }, - { - key_as_string: '2015-01-31T09:00:00.000Z', - key: 1422694800000, - doc_count: 1419, - }, - { - key_as_string: '2015-01-31T10:00:00.000Z', - key: 1422698400000, - doc_count: 1725, - }, - { - key_as_string: '2015-01-31T11:00:00.000Z', - key: 1422702000000, - doc_count: 1801, - }, - { - key_as_string: '2015-01-31T12:00:00.000Z', - key: 1422705600000, - doc_count: 1823, - }, - { - key_as_string: '2015-01-31T13:00:00.000Z', - key: 1422709200000, - doc_count: 1657, - }, - { - key_as_string: '2015-01-31T14:00:00.000Z', - key: 1422712800000, - doc_count: 1454, - }, - { - key_as_string: '2015-01-31T15:00:00.000Z', - key: 1422716400000, - doc_count: 1131, - }, - { - key_as_string: '2015-01-31T16:00:00.000Z', - key: 1422720000000, - doc_count: 810, - }, - { - key_as_string: '2015-01-31T17:00:00.000Z', - key: 1422723600000, - doc_count: 583, - }, - { - key_as_string: '2015-01-31T18:00:00.000Z', - key: 1422727200000, - doc_count: 384, - }, - { - key_as_string: '2015-01-31T19:00:00.000Z', - key: 1422730800000, - doc_count: 165, - }, - { - key_as_string: '2015-01-31T20:00:00.000Z', - key: 1422734400000, - doc_count: 135, - }, - { - key_as_string: '2015-01-31T21:00:00.000Z', - key: 1422738000000, - doc_count: 72, - }, - { - key_as_string: '2015-01-31T22:00:00.000Z', - key: 1422741600000, - doc_count: 8, - }, - ], - }, - }, -}; diff --git a/src/fixtures/agg_resp/geohash_grid.js b/src/fixtures/agg_resp/geohash_grid.js deleted file mode 100644 index 4a8fb3704c9b3..0000000000000 --- a/src/fixtures/agg_resp/geohash_grid.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -export default function GeoHashGridAggResponseFixture() { - // for vis: - // - // vis = new Vis(indexPattern, { - // type: 'tile_map', - // aggs:[ - // { schema: 'metric', type: 'avg', params: { field: 'bytes' } }, - // { schema: 'split', type: 'terms', params: { field: '@tags', size: 10 } }, - // { schema: 'segment', type: 'geohash_grid', params: { field: 'geo.coordinates', precision: 3 } } - // ], - // params: { - // isDesaturated: true, - // mapType: 'Scaled%20Circle%20Markers' - // }, - // }); - - const geoHashCharts = _.union( - _.range(48, 57), // 0-9 - _.range(65, 90), // A-Z - _.range(97, 122) // a-z - ); - - const tags = _.times(_.random(4, 20), function (i) { - // random number of tags - let docCount = 0; - const buckets = _.times(_.random(40, 200), function () { - return _.sampleSize(geoHashCharts, 3).join(''); - }) - .sort() - .map(function (geoHash) { - const count = _.random(1, 5000); - - docCount += count; - - return { - key: geoHash, - doc_count: count, - 1: { - value: 2048 + i, - }, - }; - }); - - return { - key: 'tag ' + (i + 1), - doc_count: docCount, - 3: { - buckets: buckets, - }, - 1: { - value: 1000 + i, - }, - }; - }); - - return { - took: 3, - timed_out: false, - _shards: { - total: 4, - successful: 4, - failed: 0, - }, - hits: { - total: 298, - max_score: 0.0, - hits: [], - }, - aggregations: { - 2: { - buckets: tags, - }, - }, - }; -} diff --git a/src/fixtures/agg_resp/range.js b/src/fixtures/agg_resp/range.js deleted file mode 100644 index ca15f535add82..0000000000000 --- a/src/fixtures/agg_resp/range.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export default { - took: 35, - timed_out: false, - _shards: { - total: 7, - successful: 7, - failed: 0, - }, - hits: { - total: 218512, - max_score: 0, - hits: [], - }, - aggregations: { - 1: { - buckets: { - '*-1024.0': { - to: 1024, - to_as_string: '1024.0', - doc_count: 20904, - }, - '1024.0-2560.0': { - from: 1024, - from_as_string: '1024.0', - to: 2560, - to_as_string: '2560.0', - doc_count: 23358, - }, - '2560.0-*': { - from: 2560, - from_as_string: '2560.0', - doc_count: 174250, - }, - }, - }, - }, -}; diff --git a/src/fixtures/config_upgrade_from_4.0.0.json b/src/fixtures/config_upgrade_from_4.0.0.json deleted file mode 100644 index 522de78648c9b..0000000000000 --- a/src/fixtures/config_upgrade_from_4.0.0.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "took": 1, - "timed_out": false, - "_shards": { - "total": 1, - "successful": 1, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 1, - "hits": [ - { - "_index": ".kibana", - "_type": "config", - "_id": "4.0.0", - "_score": 1, - "_source": { - "buildNum": 5888, - "defaultIndex": "logstash-*" - } - } - ] - } -} diff --git a/src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json b/src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json deleted file mode 100644 index 8767232dcdc1c..0000000000000 --- a/src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "took": 1, - "timed_out": false, - "_shards": { - "total": 1, - "successful": 1, - "failed": 0 - }, - "hits": { - "total": 2, - "max_score": 1, - "hits": [ - { - "_index": ".kibana", - "_type": "config", - "_id": "4.0.1-SNAPSHOT", - "_score": 1, - "_source": { - "buildNum": 5921, - "defaultIndex": "logstash-*" - } - }, - { - "_index": ".kibana", - "_type": "config", - "_id": "4.0.0", - "_score": 1, - "_source": { - "buildNum": 5888, - "defaultIndex": "logstash-*" - } - } - ] - } -} diff --git a/src/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json b/src/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json deleted file mode 100644 index 57b486491b397..0000000000000 --- a/src/fixtures/config_upgrade_from_4.0.0_to_4.0.1.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "took": 1, - "timed_out": false, - "_shards": { - "total": 1, - "successful": 1, - "failed": 0 - }, - "hits": { - "total": 2, - "max_score": 1, - "hits": [ - { - "_index": ".kibana", - "_type": "config", - "_id": "4.0.1", - "_score": 1, - "_source": { - "buildNum": 5921, - "defaultIndex": "logstash-*" - } - }, - { - "_index": ".kibana", - "_type": "config", - "_id": "4.0.0", - "_score": 1, - "_source": { - "buildNum": 5888, - "defaultIndex": "logstash-*" - } - } - ] - } -} diff --git a/src/fixtures/fake_chart_events.js b/src/fixtures/fake_chart_events.js deleted file mode 100644 index 71f49cb4713b8..0000000000000 --- a/src/fixtures/fake_chart_events.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -const results = {}; - -results.timeSeries = { - data: { - ordered: { - date: true, - interval: 600000, - max: 1414437217559, - min: 1414394017559, - }, - }, - label: 'apache', - value: 44, - point: { - label: 'apache', - x: 1414400400000, - y: 44, - y0: 0, - }, -}; diff --git a/src/fixtures/fake_hierarchical_data.ts b/src/fixtures/fake_hierarchical_data.ts deleted file mode 100644 index 2e23acfc3a803..0000000000000 --- a/src/fixtures/fake_hierarchical_data.ts +++ /dev/null @@ -1,621 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const metricOnly = { - hits: { total: 1000, hits: [], max_score: 0 }, - aggregations: { - agg_1: { value: 412032 }, - }, -}; - -export const threeTermBuckets = { - hits: { total: 1000, hits: [], max_score: 0 }, - aggregations: { - agg_2: { - buckets: [ - { - key: 'png', - doc_count: 50, - agg_1: { value: 412032 }, - agg_3: { - buckets: [ - { - key: 'IT', - doc_count: 10, - agg_1: { value: 9299 }, - agg_4: { - buckets: [ - { key: 'win', doc_count: 4, agg_1: { value: 0 } }, - { key: 'mac', doc_count: 6, agg_1: { value: 9299 } }, - ], - }, - }, - { - key: 'US', - doc_count: 20, - agg_1: { value: 8293 }, - agg_4: { - buckets: [ - { key: 'linux', doc_count: 12, agg_1: { value: 3992 } }, - { key: 'mac', doc_count: 8, agg_1: { value: 3029 } }, - ], - }, - }, - ], - }, - }, - { - key: 'css', - doc_count: 20, - agg_1: { value: 412032 }, - agg_3: { - buckets: [ - { - key: 'MX', - doc_count: 7, - agg_1: { value: 9299 }, - agg_4: { - buckets: [ - { key: 'win', doc_count: 3, agg_1: { value: 4992 } }, - { key: 'mac', doc_count: 4, agg_1: { value: 5892 } }, - ], - }, - }, - { - key: 'US', - doc_count: 13, - agg_1: { value: 8293 }, - agg_4: { - buckets: [ - { key: 'linux', doc_count: 12, agg_1: { value: 3992 } }, - { key: 'mac', doc_count: 1, agg_1: { value: 3029 } }, - ], - }, - }, - ], - }, - }, - { - key: 'html', - doc_count: 90, - agg_1: { value: 412032 }, - agg_3: { - buckets: [ - { - key: 'CN', - doc_count: 85, - agg_1: { value: 9299 }, - agg_4: { - buckets: [ - { key: 'win', doc_count: 46, agg_1: { value: 4992 } }, - { key: 'mac', doc_count: 39, agg_1: { value: 5892 } }, - ], - }, - }, - { - key: 'FR', - doc_count: 15, - agg_1: { value: 8293 }, - agg_4: { - buckets: [ - { key: 'win', doc_count: 3, agg_1: { value: 3992 } }, - { key: 'mac', doc_count: 12, agg_1: { value: 3029 } }, - ], - }, - }, - ], - }, - }, - ], - }, - }, -}; - -export const oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = { - hits: { total: 1000, hits: [], max_score: 0 }, - aggregations: { - agg_3: { - buckets: [ - { - key: 'png', - doc_count: 50, - agg_4: { - buckets: [ - { - key_as_string: '2014-09-28T00:00:00.000Z', - key: 1411862400000, - doc_count: 1, - agg_1: { value: 9283 }, - agg_2: { value: 1411862400000 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 23, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-09-29T00:00:00.000Z', - key: 1411948800000, - doc_count: 2, - agg_1: { value: 28349 }, - agg_2: { value: 1411948800000 }, - agg_5: { value: 203 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 39, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-09-30T00:00:00.000Z', - key: 1412035200000, - doc_count: 3, - agg_1: { value: 84330 }, - agg_2: { value: 1412035200000 }, - agg_5: { value: 200 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 329, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-01T00:00:00.000Z', - key: 1412121600000, - doc_count: 4, - agg_1: { value: 34992 }, - agg_2: { value: 1412121600000 }, - agg_5: { value: 103 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 22, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-02T00:00:00.000Z', - key: 1412208000000, - doc_count: 5, - agg_1: { value: 145432 }, - agg_2: { value: 1412208000000 }, - agg_5: { value: 153 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 93, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-03T00:00:00.000Z', - key: 1412294400000, - doc_count: 35, - agg_1: { value: 220943 }, - agg_2: { value: 1412294400000 }, - agg_5: { value: 239 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 72, - }, - }, - ], - }, - }, - }, - ], - }, - }, - { - key: 'css', - doc_count: 20, - agg_4: { - buckets: [ - { - key_as_string: '2014-09-28T00:00:00.000Z', - key: 1411862400000, - doc_count: 1, - agg_1: { value: 9283 }, - agg_2: { value: 1411862400000 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 75, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-09-29T00:00:00.000Z', - key: 1411948800000, - doc_count: 2, - agg_1: { value: 28349 }, - agg_2: { value: 1411948800000 }, - agg_5: { value: 10 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 11, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-09-30T00:00:00.000Z', - key: 1412035200000, - doc_count: 3, - agg_1: { value: 84330 }, - agg_2: { value: 1412035200000 }, - agg_5: { value: 24 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 238, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-01T00:00:00.000Z', - key: 1412121600000, - doc_count: 4, - agg_1: { value: 34992 }, - agg_2: { value: 1412121600000 }, - agg_5: { value: 49 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 343, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-02T00:00:00.000Z', - key: 1412208000000, - doc_count: 5, - agg_1: { value: 145432 }, - agg_2: { value: 1412208000000 }, - agg_5: { value: 100 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 837, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-03T00:00:00.000Z', - key: 1412294400000, - doc_count: 5, - agg_1: { value: 220943 }, - agg_2: { value: 1412294400000 }, - agg_5: { value: 23 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 302, - }, - }, - ], - }, - }, - }, - ], - }, - }, - { - key: 'html', - doc_count: 90, - agg_4: { - buckets: [ - { - key_as_string: '2014-09-28T00:00:00.000Z', - key: 1411862400000, - doc_count: 10, - agg_1: { value: 9283 }, - agg_2: { value: 1411862400000 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 30, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-09-29T00:00:00.000Z', - key: 1411948800000, - doc_count: 20, - agg_1: { value: 28349 }, - agg_2: { value: 1411948800000 }, - agg_5: { value: 1 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 43, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-09-30T00:00:00.000Z', - key: 1412035200000, - doc_count: 30, - agg_1: { value: 84330 }, - agg_2: { value: 1412035200000 }, - agg_5: { value: 5 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 88, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-01T00:00:00.000Z', - key: 1412121600000, - doc_count: 11, - agg_1: { value: 34992 }, - agg_2: { value: 1412121600000 }, - agg_5: { value: 10 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 91, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-02T00:00:00.000Z', - key: 1412208000000, - doc_count: 12, - agg_1: { value: 145432 }, - agg_2: { value: 1412208000000 }, - agg_5: { value: 43 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 534, - }, - }, - ], - }, - }, - }, - { - key_as_string: '2014-10-03T00:00:00.000Z', - key: 1412294400000, - doc_count: 7, - agg_1: { value: 220943 }, - agg_2: { value: 1412294400000 }, - agg_5: { value: 1 }, - agg_6: { - hits: { - total: 2, - hits: [ - { - fields: { - bytes: 553, - }, - }, - ], - }, - }, - }, - ], - }, - }, - ], - }, - }, -}; - -export const oneRangeBucket = { - took: 35, - timed_out: false, - _shards: { - total: 1, - successful: 1, - failed: 0, - }, - hits: { - total: 6039, - max_score: 0, - hits: [], - }, - aggregations: { - agg_2: { - buckets: { - '0.0-1000.0': { - from: 0, - from_as_string: '0.0', - to: 1000, - to_as_string: '1000.0', - doc_count: 606, - }, - '1000.0-2000.0': { - from: 1000, - from_as_string: '1000.0', - to: 2000, - to_as_string: '2000.0', - doc_count: 298, - }, - }, - }, - }, -}; - -export const oneFilterBucket = { - took: 11, - timed_out: false, - _shards: { - total: 1, - successful: 1, - failed: 0, - }, - hits: { - total: 6005, - max_score: 0, - hits: [], - }, - aggregations: { - agg_2: { - buckets: { - 'type:apache': { - doc_count: 4844, - }, - 'type:nginx': { - doc_count: 1161, - }, - }, - }, - }, -}; - -export const oneHistogramBucket = { - took: 37, - timed_out: false, - _shards: { - total: 6, - successful: 6, - failed: 0, - }, - hits: { - total: 49208, - max_score: 0, - hits: [], - }, - aggregations: { - agg_2: { - buckets: [ - { - key_as_string: '2014-09-28T00:00:00.000Z', - key: 1411862400000, - doc_count: 8247, - }, - { - key_as_string: '2014-09-29T00:00:00.000Z', - key: 1411948800000, - doc_count: 8184, - }, - { - key_as_string: '2014-09-30T00:00:00.000Z', - key: 1412035200000, - doc_count: 8269, - }, - { - key_as_string: '2014-10-01T00:00:00.000Z', - key: 1412121600000, - doc_count: 8141, - }, - { - key_as_string: '2014-10-02T00:00:00.000Z', - key: 1412208000000, - doc_count: 8148, - }, - { - key_as_string: '2014-10-03T00:00:00.000Z', - key: 1412294400000, - doc_count: 8219, - }, - ], - }, - }, -}; diff --git a/src/fixtures/field_mapping.js b/src/fixtures/field_mapping.js deleted file mode 100644 index 5077e361d5458..0000000000000 --- a/src/fixtures/field_mapping.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export default { - test: { - mappings: { - testType: { - baz: { - full_name: 'baz', - mapping: { - bar: { - type: 'long', - }, - }, - }, - 'foo.bar': { - full_name: 'foo.bar', - mapping: { - bar: { - type: 'string', - }, - }, - }, - not_analyzed_field: { - full_name: 'not_analyzed_field', - mapping: { - bar: { - type: 'string', - index: 'not_analyzed', - }, - }, - }, - index_no_field: { - full_name: 'index_no_field', - mapping: { - bar: { - type: 'string', - index: 'no', - }, - }, - }, - _id: { - full_name: '_id', - mapping: { - _id: { - store: false, - index: 'no', - }, - }, - }, - _timestamp: { - full_name: '_timestamp', - mapping: { - _timestamp: { - store: true, - index: 'no', - }, - }, - }, - }, - }, - }, -}; diff --git a/src/fixtures/hits.js b/src/fixtures/hits.js deleted file mode 100644 index af8264e320909..0000000000000 --- a/src/fixtures/hits.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export default function fitsFixture() { - return [ - // extension - // | machine.os - //timestamp | | bytes - //| ssl ip | | | request - [0, true, '192.168.0.1', 'php', 'Linux', 10, 'foo'], - [1, true, '192.168.0.1', 'php', 'Linux', 20, 'bar'], - [2, true, '192.168.0.1', 'php', 'Linux', 30, 'bar'], - [3, true, '192.168.0.1', 'php', 'Linux', 30, 'baz'], - [4, true, '192.168.0.1', 'php', 'Linux', 30, 'baz'], - [5, true, '192.168.0.1', 'php', 'Linux', 40.141592, 'bat'], - [6, true, '192.168.0.1', 'php', 'Linux', 40.141592, 'bat'], - [7, true, '192.168.0.1', 'php', 'Linux', 40.141592, 'bat'], - [8, true, '192.168.0.1', 'php', 'Linux', 40.141592, 'bat'], - [9, true, '192.168.0.1', 'php', 'Linux', 40.141592, 'bat'], - ].map((row, i) => { - return { - _score: 1, - _id: 1000 + i, - _index: 'test-index', - _source: { - '@timestamp': row[0], - ssl: row[1], - ip: row[2], - extension: row[3], - 'machine.os': row[4], - bytes: row[5], - request: row[6], - }, - }; - }); -} diff --git a/src/fixtures/mapping_with_dupes.js b/src/fixtures/mapping_with_dupes.js deleted file mode 100644 index 7f6da2600c9a8..0000000000000 --- a/src/fixtures/mapping_with_dupes.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export default { - test: { - mappings: { - testType: { - baz: { - full_name: 'baz', - mapping: { - bar: { - type: 'long', - }, - }, - }, - 'foo.bar': { - full_name: 'foo.bar', - mapping: { - bar: { - type: 'string', - }, - }, - }, - }, - }, - }, - duplicates: { - mappings: { - testType: { - baz: { - full_name: 'baz', - mapping: { - bar: { - type: 'date', - }, - }, - }, - }, - }, - }, -}; diff --git a/src/fixtures/mock_index_patterns.js b/src/fixtures/mock_index_patterns.js deleted file mode 100644 index ce44b71613b01..0000000000000 --- a/src/fixtures/mock_index_patterns.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import sinon from 'sinon'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -export default function (Private) { - const indexPatterns = Private(FixturesStubbedLogstashIndexPatternProvider); - const getIndexPatternStub = sinon.stub().resolves(indexPatterns); - - return { - get: getIndexPatternStub, - }; -} diff --git a/src/fixtures/mock_state.js b/src/fixtures/mock_state.js deleted file mode 100644 index cb18dac7b767d..0000000000000 --- a/src/fixtures/mock_state.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import _ from 'lodash'; -import sinon from 'sinon'; - -function MockState(defaults) { - this.on = _.noop; - this.off = _.noop; - this.save = sinon.stub(); - this.replace = sinon.stub(); - _.assign(this, defaults); -} - -export default MockState; diff --git a/src/fixtures/mock_ui_state.js b/src/fixtures/mock_ui_state.js deleted file mode 100644 index fc0a18137a5fd..0000000000000 --- a/src/fixtures/mock_ui_state.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { set } from '@elastic/safer-lodash-set'; -import _ from 'lodash'; -let values = {}; -export default { - get: function (path, def) { - return _.get(values, path, def); - }, - set: function (path, val) { - set(values, path, val); - return val; - }, - setSilent: function (path, val) { - set(values, path, val); - return val; - }, - emit: _.noop, - on: _.noop, - off: _.noop, - clearAllKeys: function () { - values = {}; - }, - _reset: function () { - values = {}; - }, -}; diff --git a/src/fixtures/search_response.js b/src/fixtures/search_response.js deleted file mode 100644 index a84bd184990e0..0000000000000 --- a/src/fixtures/search_response.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import hits from 'fixtures/real_hits'; - -export default { - took: 73, - timed_out: false, - _shards: { - total: 144, - successful: 144, - failed: 0, - }, - hits: { - total: 49487, - max_score: 1.0, - hits: hits, - }, -}; diff --git a/src/fixtures/stubbed_search_source.js b/src/fixtures/stubbed_search_source.js deleted file mode 100644 index ea41e7bbe681c..0000000000000 --- a/src/fixtures/stubbed_search_source.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import sinon from 'sinon'; -import searchResponse from 'fixtures/search_response'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -export default function stubSearchSource(Private, $q, Promise) { - let deferedResult = $q.defer(); - const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - let onResultsCount = 0; - return { - setField: sinon.spy(), - fetch: sinon.spy(), - destroy: sinon.spy(), - getField: function (param) { - switch (param) { - case 'index': - return indexPattern; - default: - throw new Error(`Param "${param}" is not implemented in the stubbed search source`); - } - }, - crankResults: function () { - deferedResult.resolve(searchResponse); - deferedResult = $q.defer(); - }, - onResults: function () { - onResultsCount++; - - // Up to the test to resolve this manually - // For example: - // someHandler.resolve(require('fixtures/search_response')) - return deferedResult.promise; - }, - getOnResultsCount: function () { - return onResultsCount; - }, - _flatten: function () { - return Promise.resolve({ index: indexPattern, body: {} }); - }, - _requestStartHandlers: [], - onRequestStart(fn) { - this._requestStartHandlers.push(fn); - }, - requestIsStopped() {}, - }; -} diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 0caaac6764bbe..786afc81c400c 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -321,6 +321,33 @@ export function DashboardTopNav({ dashboardStateManager, ]); + const runQuickSave = useCallback(async () => { + const currentTitle = dashboardStateManager.getTitle(); + const currentDescription = dashboardStateManager.getDescription(); + const currentTimeRestore = dashboardStateManager.getTimeRestore(); + + let currentTags: string[] = []; + if (savedObjectsTagging) { + const dashboard = dashboardStateManager.savedDashboard; + if (savedObjectsTagging.ui.hasTagDecoration(dashboard)) { + currentTags = dashboard.getTags(); + } + } + + save({}).then((response: SaveResult) => { + // If the save wasn't successful, put the original values back. + if (!(response as { id: string }).id) { + dashboardStateManager.setTitle(currentTitle); + dashboardStateManager.setDescription(currentDescription); + dashboardStateManager.setTimeRestore(currentTimeRestore); + if (savedObjectsTagging) { + dashboardStateManager.setTags(currentTags); + } + } + return response; + }); + }, [save, savedObjectsTagging, dashboardStateManager]); + const runClone = useCallback(() => { const currentTitle = dashboardStateManager.getTitle(); const onClone = async ( @@ -356,9 +383,8 @@ export function DashboardTopNav({ [TopNavIds.ENTER_EDIT_MODE]: () => onChangeViewMode(ViewMode.EDIT), [TopNavIds.DISCARD_CHANGES]: onDiscardChanges, [TopNavIds.SAVE]: runSave, + [TopNavIds.QUICK_SAVE]: runQuickSave, [TopNavIds.CLONE]: runClone, - [TopNavIds.ADD_EXISTING]: addFromLibrary, - [TopNavIds.VISUALIZE]: createNew, [TopNavIds.OPTIONS]: (anchorElement) => { showOptionsPopover({ anchorElement, @@ -394,10 +420,9 @@ export function DashboardTopNav({ onDiscardChanges, onChangeViewMode, savedDashboard, - addFromLibrary, - createNew, runClone, runSave, + runQuickSave, share, ]); @@ -419,11 +444,11 @@ export function DashboardTopNav({ const showFilterBar = shouldShowFilterBar(Boolean(embedSettings?.forceHideFilterBar)); const showSearchBar = showQueryBar || showFilterBar; - const topNav = getTopNavConfig( - viewMode, - dashboardTopNavActions, - dashboardCapabilities.hideWriteControls - ); + const topNav = getTopNavConfig(viewMode, dashboardTopNavActions, { + hideWriteControls: dashboardCapabilities.hideWriteControls, + isNewDashboard: !savedDashboard.id, + isDirty: dashboardStateManager.isDirty, + }); return { appName: 'dashboard', diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index 37414cb948d5a..abc128369017c 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -20,11 +20,11 @@ import { NavAction } from '../../types'; export function getTopNavConfig( dashboardMode: ViewMode, actions: { [key: string]: NavAction }, - hideWriteControls: boolean + options: { hideWriteControls: boolean; isNewDashboard: boolean; isDirty: boolean } ) { switch (dashboardMode) { case ViewMode.VIEW: - return hideWriteControls + return options.hideWriteControls ? [ getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), getShareConfig(actions[TopNavIds.SHARE]), @@ -36,20 +36,39 @@ export function getTopNavConfig( getEditConfig(actions[TopNavIds.ENTER_EDIT_MODE]), ]; case ViewMode.EDIT: - return [ - getOptionsConfig(actions[TopNavIds.OPTIONS]), - getShareConfig(actions[TopNavIds.SHARE]), - getAddConfig(actions[TopNavIds.ADD_EXISTING]), - getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), - getDiscardConfig(actions[TopNavIds.DISCARD_CHANGES]), - getSaveConfig(actions[TopNavIds.SAVE]), - getCreateNewConfig(actions[TopNavIds.VISUALIZE]), - ]; + return options.isNewDashboard + ? [ + getOptionsConfig(actions[TopNavIds.OPTIONS]), + getShareConfig(actions[TopNavIds.SHARE]), + getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), + getDiscardConfig(actions[TopNavIds.DISCARD_CHANGES]), + getSaveConfig(actions[TopNavIds.SAVE], options.isNewDashboard), + ] + : [ + getOptionsConfig(actions[TopNavIds.OPTIONS]), + getShareConfig(actions[TopNavIds.SHARE]), + getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]), + getDiscardConfig(actions[TopNavIds.DISCARD_CHANGES]), + getSaveConfig(actions[TopNavIds.SAVE]), + getQuickSave(actions[TopNavIds.QUICK_SAVE]), + ]; default: return []; } } +function getSaveButtonLabel() { + return i18n.translate('dashboard.topNave.saveButtonAriaLabel', { + defaultMessage: 'save', + }); +} + +function getSaveAsButtonLabel() { + return i18n.translate('dashboard.topNave.saveAsButtonAriaLabel', { + defaultMessage: 'save as', + }); +} + function getFullScreenConfig(action: NavAction) { return { id: 'full-screen', @@ -89,17 +108,32 @@ function getEditConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getSaveConfig(action: NavAction) { +function getQuickSave(action: NavAction) { return { - id: 'save', - label: i18n.translate('dashboard.topNave.saveButtonAriaLabel', { - defaultMessage: 'save', - }), + id: 'quick-save', + emphasize: true, + label: getSaveButtonLabel(), description: i18n.translate('dashboard.topNave.saveConfigDescription', { - defaultMessage: 'Save your dashboard', + defaultMessage: 'Quick save your dashboard without any prompts', + }), + testId: 'dashboardQuickSaveMenuItem', + run: action, + }; +} + +/** + * @returns {kbnTopNavConfig} + */ +function getSaveConfig(action: NavAction, isNewDashboard = false) { + return { + id: 'save', + label: isNewDashboard ? getSaveButtonLabel() : getSaveAsButtonLabel(), + description: i18n.translate('dashboard.topNave.saveAsConfigDescription', { + defaultMessage: 'Save as a new dashboard', }), testId: 'dashboardSaveMenuItem', run: action, + emphasize: isNewDashboard, }; } @@ -157,42 +191,6 @@ function getCloneConfig(action: NavAction) { /** * @returns {kbnTopNavConfig} */ -function getAddConfig(action: NavAction) { - return { - id: 'add', - label: i18n.translate('dashboard.topNave.addButtonAriaLabel', { - defaultMessage: 'Library', - }), - description: i18n.translate('dashboard.topNave.addConfigDescription', { - defaultMessage: 'Add an existing visualization to the dashboard', - }), - testId: 'dashboardAddPanelButton', - run: action, - }; -} - -/** - * @returns {kbnTopNavConfig} - */ -function getCreateNewConfig(action: NavAction) { - return { - emphasize: true, - iconType: 'plusInCircleFilled', - id: 'addNew', - label: i18n.translate('dashboard.topNave.addNewButtonAriaLabel', { - defaultMessage: 'Create panel', - }), - description: i18n.translate('dashboard.topNave.addNewConfigDescription', { - defaultMessage: 'Create a new panel on this dashboard', - }), - testId: 'dashboardAddNewPanelButton', - run: action, - }; -} - -// /** -// * @returns {kbnTopNavConfig} -// */ function getShareConfig(action: NavAction | undefined) { return { id: 'share', diff --git a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot b/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot index f822a7e70d523..afbbecb3935e0 100644 --- a/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot +++ b/src/plugins/dashboard/public/application/top_nav/panel_toolbar/__snapshots__/panel_toolbar.stories.storyshot @@ -10,7 +10,7 @@ exports[`Storyshots components/PanelToolbar default 1`] = ` >