Merge branch 'master' into write-to-es-strawman

This commit is contained in:
Kibana Machine 2020-12-04 12:39:29 -05:00 committed by GitHub
commit 3a16056457
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
801 changed files with 18775 additions and 5910 deletions

View file

@ -51,9 +51,17 @@ You can request to overwrite any objects that already exist in the target space
(Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target
spaces. The default value is `false`.
`createNewCopies`::
(Optional, boolean) Creates new copies of saved objects, regenerates each object ID, and resets the origin. When used, potential conflict
errors are avoided. The default value is `true`.
+
NOTE: This cannot be used with the `overwrite` option.
`overwrite`::
(Optional, boolean) When set to `true`, all conflicts are automatically overidden. When a saved object with a matching `type` and `id`
exists in the target space, that version is replaced with the version from the source space. The default value is `false`.
+
NOTE: This cannot be used with the `createNewCopies` option.
[role="child_attributes"]
[[spaces-api-copy-saved-objects-response-body]]
@ -128,8 +136,7 @@ $ curl -X POST api/spaces/_copy_saved_objects
"id": "my-dashboard"
}],
"spaces": ["marketing"],
"includeReferences": true,
"createNewcopies": true
"includeReferences": true
}
----
// KIBANA
@ -193,7 +200,8 @@ $ curl -X POST api/spaces/_copy_saved_objects
"id": "my-dashboard"
}],
"spaces": ["marketing"],
"includeReferences": true
"includeReferences": true,
"createNewCopies": false
}
----
// KIBANA
@ -254,7 +262,8 @@ $ curl -X POST api/spaces/_copy_saved_objects
"id": "my-dashboard"
}],
"spaces": ["marketing", "sales"],
"includeReferences": true
"includeReferences": true,
"createNewCopies": false
}
----
// KIBANA
@ -405,7 +414,8 @@ $ curl -X POST api/spaces/_copy_saved_objects
"id": "my-dashboard"
}],
"spaces": ["marketing"],
"includeReferences": true
"includeReferences": true,
"createNewCopies": false
}
----
// KIBANA

View file

@ -45,6 +45,10 @@ Execute the <<spaces-api-copy-saved-objects,copy saved objects to space API>>, w
`includeReferences`::
(Optional, boolean) When set to `true`, all saved objects related to the specified saved objects are copied into the target spaces. The `includeReferences` must be the same values used during the failed <<spaces-api-copy-saved-objects, copy saved objects to space API>> operation. The default value is `false`.
`createNewCopies`::
(Optional, boolean) Creates new copies of the saved objects, regenerates each object ID, and resets the origin. When enabled during the
initial copy, also enable when resolving copy errors. The default value is `true`.
`retries`::
(Required, object) The retry operations to attempt, which can specify how to resolve different types of errors. Object keys represent the
target space IDs.
@ -148,6 +152,7 @@ $ curl -X POST api/spaces/_resolve_copy_saved_objects_errors
"id": "my-dashboard"
}],
"includeReferences": true,
"createNewCopies": false,
"retries": {
"sales": [
{
@ -246,6 +251,7 @@ $ curl -X POST api/spaces/_resolve_copy_saved_objects_errors
"id": "my-dashboard"
}],
"includeReferences": true,
"createNewCopies": false,
"retries": {
"marketing": [
{

View file

@ -110,6 +110,20 @@ View all available options by running `yarn start --help`
Read about more advanced options for <<running-kibana-advanced>>.
[discrete]
=== Install pre-commit hook (optional)
In case you want to run a couple of checks like linting or check the file casing of the files to commit, we provide
a way to install a pre-commit hook. To configure it you just need to run the following:
[source,bash]
----
node scripts/register_git_hook
----
After the script completes the pre-commit hook will be created within the file `.git/hooks/pre-commit`.
If you choose to not install it, don't worry, we still run a quick ci check to provide feedback earliest as we can about the same checks.
[discrete]
=== Code away!

View file

@ -19,7 +19,7 @@ export interface UiSettingsParams<T = unknown>
| [category](./kibana-plugin-core-public.uisettingsparams.category.md) | <code>string[]</code> | used to group the configured setting in the UI |
| [deprecation](./kibana-plugin-core-public.uisettingsparams.deprecation.md) | <code>DeprecationSettings</code> | optional deprecation information. Used to generate a deprecation warning. |
| [description](./kibana-plugin-core-public.uisettingsparams.description.md) | <code>string</code> | description provided to a user in UI |
| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiStatsMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiCounterMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
| [name](./kibana-plugin-core-public.uisettingsparams.name.md) | <code>string</code> | title in the UI |
| [optionLabels](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | <code>Record&lt;string, string&gt;</code> | text labels for 'select' type UI element |
| [options](./kibana-plugin-core-public.uisettingsparams.options.md) | <code>string[]</code> | array of permitted values for this setting |

View file

@ -15,7 +15,7 @@ Metric to track once this property changes
```typescript
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
```

View file

@ -177,6 +177,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [SavedObjectsImportSuccess](./kibana-plugin-core-server.savedobjectsimportsuccess.md) | Represents a successful import. |
| [SavedObjectsImportUnknownError](./kibana-plugin-core-server.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. |
| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-core-server.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. |
| [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) | |
| [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) | |
| [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) | |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) &gt; [fieldName](./kibana-plugin-core-server.savedobjectsincrementcounterfield.fieldname.md)
## SavedObjectsIncrementCounterField.fieldName property
The field name to increment the counter by.
<b>Signature:</b>
```typescript
fieldName: string;
```

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) &gt; [incrementBy](./kibana-plugin-core-server.savedobjectsincrementcounterfield.incrementby.md)
## SavedObjectsIncrementCounterField.incrementBy property
The number to increment the field by (defaults to 1).
<b>Signature:</b>
```typescript
incrementBy?: number;
```

View file

@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md)
## SavedObjectsIncrementCounterField interface
<b>Signature:</b>
```typescript
export interface SavedObjectsIncrementCounterField
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [fieldName](./kibana-plugin-core-server.savedobjectsincrementcounterfield.fieldname.md) | <code>string</code> | The field name to increment the counter by. |
| [incrementBy](./kibana-plugin-core-server.savedobjectsincrementcounterfield.incrementby.md) | <code>number</code> | The number to increment the field by (defaults to 1). |

View file

@ -4,12 +4,12 @@
## SavedObjectsRepository.incrementCounter() method
Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id.
Increments all the specified counter fields (by one by default). Creates the document if one doesn't exist for the given id.
<b>Signature:</b>
```typescript
incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject>;
incrementCounter<T = unknown>(type: string, id: string, counterFields: Array<string | SavedObjectsIncrementCounterField>, options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
```
## Parameters
@ -18,12 +18,12 @@ incrementCounter(type: string, id: string, counterFieldNames: string[], options?
| --- | --- | --- |
| type | <code>string</code> | The type of saved object whose fields should be incremented |
| id | <code>string</code> | The id of the document whose fields should be incremented |
| counterFieldNames | <code>string[]</code> | An array of field names to increment |
| counterFields | <code>Array&lt;string &#124; SavedObjectsIncrementCounterField&gt;</code> | An array of field names to increment or an array of [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) |
| options | <code>SavedObjectsIncrementCounterOptions</code> | [SavedObjectsIncrementCounterOptions](./kibana-plugin-core-server.savedobjectsincrementcounteroptions.md) |
<b>Returns:</b>
`Promise<SavedObject>`
`Promise<SavedObject<T>>`
The saved object after the specified fields were incremented

View file

@ -26,7 +26,7 @@ export declare class SavedObjectsRepository
| [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[<code>addToNamespaces</code>\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. |
| [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, counterFieldNames, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increments all the specified counter fields by one. Creates the document if one doesn't exist for the given id. |
| [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. |
| [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. |
| [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object |

View file

@ -19,7 +19,7 @@ export interface UiSettingsParams<T = unknown>
| [category](./kibana-plugin-core-server.uisettingsparams.category.md) | <code>string[]</code> | used to group the configured setting in the UI |
| [deprecation](./kibana-plugin-core-server.uisettingsparams.deprecation.md) | <code>DeprecationSettings</code> | optional deprecation information. Used to generate a deprecation warning. |
| [description](./kibana-plugin-core-server.uisettingsparams.description.md) | <code>string</code> | description provided to a user in UI |
| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiStatsMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | <code>{</code><br/><code> type: UiCounterMetricType;</code><br/><code> name: string;</code><br/><code> }</code> | Metric to track once this property changes |
| [name](./kibana-plugin-core-server.uisettingsparams.name.md) | <code>string</code> | title in the UI |
| [optionLabels](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | <code>Record&lt;string, string&gt;</code> | text labels for 'select' type UI element |
| [options](./kibana-plugin-core-server.uisettingsparams.options.md) | <code>string[]</code> | array of permitted values for this setting |

View file

@ -15,7 +15,7 @@ Metric to track once this property changes
```typescript
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
```

View file

@ -89,7 +89,7 @@
| [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | |
| [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | |
| [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | |
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object |
| [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in backgroundSearch saved object |
| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields |
| [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* |
| [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* |

View file

@ -1,6 +1,6 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) &gt; [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) &gt; [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md)
## SearchSessionInfoProvider.getName property

View file

@ -1,6 +1,6 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) &gt; [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) &gt; [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md)
## SearchSessionInfoProvider.getUrlGeneratorData property

View file

@ -1,6 +1,6 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md)
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md)
## SearchSessionInfoProvider interface
@ -16,6 +16,6 @@ export interface SearchSessionInfoProvider<ID extends UrlGeneratorId = UrlGenera
| Property | Type | Description |
| --- | --- | --- |
| [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md) | <code>() =&gt; Promise&lt;string&gt;</code> | User-facing name of the session. e.g. will be displayed in background sessions management list |
| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md) | <code>() =&gt; Promise&lt;{</code><br/><code> urlGeneratorId: ID;</code><br/><code> initialState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> restoreState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> }&gt;</code> | |
| [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | <code>() =&gt; Promise&lt;string&gt;</code> | User-facing name of the session. e.g. will be displayed in background sessions management list |
| [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md) | <code>() =&gt; Promise&lt;{</code><br/><code> urlGeneratorId: ID;</code><br/><code> initialState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> restoreState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> }&gt;</code> | |

View file

@ -13,7 +13,7 @@ getFields(): {
type?: string | undefined;
query?: import("../..").Query | undefined;
filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined;
sort?: Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric> | Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric>[] | undefined;
sort?: Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection> | Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection>[] | undefined;
highlight?: any;
highlightAll?: boolean | undefined;
aggs?: any;
@ -21,7 +21,8 @@ getFields(): {
size?: number | undefined;
source?: string | boolean | string[] | undefined;
version?: boolean | undefined;
fields?: string | boolean | string[] | undefined;
fields?: SearchFieldValue[] | undefined;
fieldsFromSource?: string | boolean | string[] | undefined;
index?: import("../..").IndexPattern | undefined;
searchAfter?: import("./types").EsQuerySearchAfter | undefined;
timeout?: string | undefined;
@ -34,7 +35,7 @@ getFields(): {
type?: string | undefined;
query?: import("../..").Query | undefined;
filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined;
sort?: Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric> | Record<string, import("./types").SortDirection | import("./types").SortDirectionNumeric>[] | undefined;
sort?: Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection> | Record<string, import("./types").SortDirectionNumeric | import("./types").SortDirection>[] | undefined;
highlight?: any;
highlightAll?: boolean | undefined;
aggs?: any;
@ -42,7 +43,8 @@ getFields(): {
size?: number | undefined;
source?: string | boolean | string[] | undefined;
version?: boolean | undefined;
fields?: string | boolean | string[] | undefined;
fields?: SearchFieldValue[] | undefined;
fieldsFromSource?: string | boolean | string[] | undefined;
index?: import("../..").IndexPattern | undefined;
searchAfter?: import("./types").EsQuerySearchAfter | undefined;
timeout?: string | undefined;

View file

@ -4,8 +4,10 @@
## SearchSourceFields.fields property
Retrieve fields via the search Fields API
<b>Signature:</b>
```typescript
fields?: NameList;
fields?: SearchFieldValue[];
```

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) &gt; [fieldsFromSource](./kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md)
## SearchSourceFields.fieldsFromSource property
> Warning: This API is now obsolete.
>
> It is recommended to use `fields` wherever possible.
>
Retreive fields directly from \_source (legacy behavior)
<b>Signature:</b>
```typescript
fieldsFromSource?: NameList;
```

View file

@ -17,7 +17,8 @@ export interface SearchSourceFields
| Property | Type | Description |
| --- | --- | --- |
| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | <code>any</code> | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) |
| [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | <code>NameList</code> | |
| [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | <code>SearchFieldValue[]</code> | Retrieve fields via the search Fields API |
| [fieldsFromSource](./kibana-plugin-plugins-data-public.searchsourcefields.fieldsfromsource.md) | <code>NameList</code> | Retreive fields directly from \_source (legacy behavior) |
| [filter](./kibana-plugin-plugins-data-public.searchsourcefields.filter.md) | <code>Filter[] &#124; Filter &#124; (() =&gt; Filter[] &#124; Filter &#124; undefined)</code> | [Filter](./kibana-plugin-plugins-data-public.filter.md) |
| [from](./kibana-plugin-plugins-data-public.searchsourcefields.from.md) | <code>number</code> | |
| [highlight](./kibana-plugin-plugins-data-public.searchsourcefields.highlight.md) | <code>any</code> | |

View file

@ -14,6 +14,6 @@ export declare class IndexPatternsService implements Plugin<void, IndexPatternsS
| Method | Modifiers | Description |
| --- | --- | --- |
| [setup(core)](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | |
| [setup(core, { expressions })](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | |
| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | |

View file

@ -7,14 +7,15 @@
<b>Signature:</b>
```typescript
setup(core: CoreSetup): void;
setup(core: CoreSetup<DataPluginStartDependencies, DataPluginStart>, { expressions }: IndexPatternsServiceSetupDeps): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| core | <code>CoreSetup</code> | |
| core | <code>CoreSetup&lt;DataPluginStartDependencies, DataPluginStart&gt;</code> | |
| { expressions } | <code>IndexPatternsServiceSetupDeps</code> | |
<b>Returns:</b>

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) &gt; [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md)
## ExecutionContext.getKibanaRequest property
Getter to retrieve the `KibanaRequest` object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user.
<b>Signature:</b>
```typescript
getKibanaRequest?: () => KibanaRequest;
```

View file

@ -17,6 +17,7 @@ export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters,
| Property | Type | Description |
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-expressions-public.executioncontext.abortsignal.md) | <code>AbortSignal</code> | Adds ability to abort current execution. |
| [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) | <code>() =&gt; KibanaRequest</code> | Getter to retrieve the <code>KibanaRequest</code> object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. |
| [getSavedObject](./kibana-plugin-plugins-expressions-public.executioncontext.getsavedobject.md) | <code>&lt;T extends SavedObjectAttributes = SavedObjectAttributes&gt;(type: string, id: string) =&gt; Promise&lt;SavedObject&lt;T&gt;&gt;</code> | Allows to fetch saved objects from ElasticSearch. In browser <code>getSavedObject</code> function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. |
| [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | <code>() =&gt; ExecutionContextSearch</code> | Get search context of the expression. |
| [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | <code>() =&gt; string &#124; undefined</code> | Search context in which expression should operate. |

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) &gt; [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) &gt; [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md)
## ExecutionContext.getKibanaRequest property
Getter to retrieve the `KibanaRequest` object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user.
<b>Signature:</b>
```typescript
getKibanaRequest?: () => KibanaRequest;
```

View file

@ -17,6 +17,7 @@ export interface ExecutionContext<InspectorAdapters extends Adapters = Adapters,
| Property | Type | Description |
| --- | --- | --- |
| [abortSignal](./kibana-plugin-plugins-expressions-server.executioncontext.abortsignal.md) | <code>AbortSignal</code> | Adds ability to abort current execution. |
| [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) | <code>() =&gt; KibanaRequest</code> | Getter to retrieve the <code>KibanaRequest</code> object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. |
| [getSavedObject](./kibana-plugin-plugins-expressions-server.executioncontext.getsavedobject.md) | <code>&lt;T extends SavedObjectAttributes = SavedObjectAttributes&gt;(type: string, id: string) =&gt; Promise&lt;SavedObject&lt;T&gt;&gt;</code> | Allows to fetch saved objects from ElasticSearch. In browser <code>getSavedObject</code> function is provided automatically by the Expressions plugin. On the server the caller of the expression has to provide this context function. The reason is because on the browser we always know the user who tries to fetch a saved object, thus saved object client is scoped automatically to that user. However, on the server we can scope that saved object client to any user, or even not scope it at all and execute it as an "internal" user. |
| [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | <code>() =&gt; ExecutionContextSearch</code> | Get search context of the expression. |
| [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | <code>() =&gt; string &#124; undefined</code> | Search context in which expression should operate. |

View file

@ -261,7 +261,9 @@ For information about {kib} memory limits, see <<production, using {kib} in a pr
[cols="2*<"]
|===
| `xpack.reporting.index`
| Reporting uses a weekly index in {es} to store the reporting job and
| *deprecated* This setting is deprecated and will be removed in 8.0. Multitenancy by changing
`kibana.index` will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy[8.0 Breaking Changes]
for more details. Reporting uses a weekly index in {es} to store the reporting job and
the report content. The index is automatically created if it does not already
exist. Configure this to a unique value, beginning with `.reporting-`, for every
{kib} instance that has a unique <<kibana-index, `kibana.index`>> setting. Defaults to `.reporting`.

View file

@ -23,7 +23,8 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { BrowserRouter as Router } from 'react-router-dom';
import {
EuiButton,
EuiButtonEmpty,
EuiCodeBlock,
EuiPage,
EuiPageBody,
EuiPageContent,
@ -32,6 +33,7 @@ import {
EuiTitle,
EuiText,
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiCheckbox,
EuiSpacer,
@ -68,6 +70,11 @@ interface SearchExamplesAppDeps {
data: DataPublicPluginStart;
}
function getNumeric(fields?: IndexPatternField[]) {
if (!fields) return [];
return fields?.filter((f) => f.type === 'number' && f.aggregatable);
}
function formatFieldToComboBox(field?: IndexPatternField | null) {
if (!field) return [];
return formatFieldsToComboBox([field]);
@ -95,8 +102,13 @@ export const SearchExamplesApp = ({
const [getCool, setGetCool] = useState<boolean>(false);
const [timeTook, setTimeTook] = useState<number | undefined>();
const [indexPattern, setIndexPattern] = useState<IndexPattern | null>();
const [numericFields, setNumericFields] = useState<IndexPatternField[]>();
const [selectedField, setSelectedField] = useState<IndexPatternField | null | undefined>();
const [fields, setFields] = useState<IndexPatternField[]>();
const [selectedFields, setSelectedFields] = useState<IndexPatternField[]>([]);
const [selectedNumericField, setSelectedNumericField] = useState<
IndexPatternField | null | undefined
>();
const [request, setRequest] = useState<Record<string, any>>({});
const [response, setResponse] = useState<Record<string, any>>({});
// Fetch the default index pattern using the `data.indexPatterns` service, as the component is mounted.
useEffect(() => {
@ -110,24 +122,23 @@ export const SearchExamplesApp = ({
// Update the fields list every time the index pattern is modified.
useEffect(() => {
const fields = indexPattern?.fields.filter(
(field) => field.type === 'number' && field.aggregatable
);
setNumericFields(fields);
setSelectedField(fields?.length ? fields[0] : null);
setFields(indexPattern?.fields);
}, [indexPattern]);
useEffect(() => {
setSelectedNumericField(fields?.length ? getNumeric(fields)[0] : null);
}, [fields]);
const doAsyncSearch = async (strategy?: string) => {
if (!indexPattern || !selectedField) return;
if (!indexPattern || !selectedNumericField) return;
// Constuct the query portion of the search request
const query = data.query.getEsQuery(indexPattern);
// Constuct the aggregations portion of the search request by using the `data.search.aggs` service.
const aggs = [{ type: 'avg', params: { field: selectedField.name } }];
const aggs = [{ type: 'avg', params: { field: selectedNumericField!.name } }];
const aggsDsl = data.search.aggs.createAggConfigs(indexPattern, aggs).toDsl();
const request = {
const req = {
params: {
index: indexPattern.title,
body: {
@ -140,23 +151,26 @@ export const SearchExamplesApp = ({
};
// Submit the search request using the `data.search` service.
setRequest(req.params.body);
const searchSubscription$ = data.search
.search(request, {
.search(req, {
strategy,
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
setTimeTook(response.rawResponse.took);
const avgResult: number | undefined = response.rawResponse.aggregations
? response.rawResponse.aggregations[1].value
next: (res) => {
if (isCompleteResponse(res)) {
setResponse(res.rawResponse);
setTimeTook(res.rawResponse.took);
const avgResult: number | undefined = res.rawResponse.aggregations
? res.rawResponse.aggregations[1].value
: undefined;
const message = (
<EuiText>
Searched {response.rawResponse.hits.total} documents. <br />
The average of {selectedField.name} is {avgResult ? Math.floor(avgResult) : 0}.
Searched {res.rawResponse.hits.total} documents. <br />
The average of {selectedNumericField!.name} is{' '}
{avgResult ? Math.floor(avgResult) : 0}.
<br />
Is it Cool? {String((response as IMyStrategyResponse).cool)}
Is it Cool? {String((res as IMyStrategyResponse).cool)}
</EuiText>
);
notifications.toasts.addSuccess({
@ -164,7 +178,7 @@ export const SearchExamplesApp = ({
text: mountReactNode(message),
});
searchSubscription$.unsubscribe();
} else if (isErrorResponse(response)) {
} else if (isErrorResponse(res)) {
// TODO: Make response error status clearer
notifications.toasts.addWarning('An error has occurred');
searchSubscription$.unsubscribe();
@ -176,6 +190,50 @@ export const SearchExamplesApp = ({
});
};
const doSearchSourceSearch = async () => {
if (!indexPattern) return;
const query = data.query.queryString.getQuery();
const filters = data.query.filterManager.getFilters();
const timefilter = data.query.timefilter.timefilter.createFilter(indexPattern);
if (timefilter) {
filters.push(timefilter);
}
try {
const searchSource = await data.search.searchSource.create();
searchSource
.setField('index', indexPattern)
.setField('filter', filters)
.setField('query', query)
.setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']);
if (selectedNumericField) {
searchSource.setField('aggs', () => {
return data.search.aggs
.createAggConfigs(indexPattern, [
{ type: 'avg', params: { field: selectedNumericField.name } },
])
.toDsl();
});
}
setRequest(await searchSource.getSearchRequestBody());
const res = await searchSource.fetch();
setResponse(res);
const message = <EuiText>Searched {res.hits.total} documents.</EuiText>;
notifications.toasts.addSuccess({
title: 'Query result',
text: mountReactNode(message),
});
} catch (e) {
setResponse(e.body);
notifications.toasts.addWarning(`An error has occurred: ${e.message}`);
}
};
const onClickHandler = () => {
doAsyncSearch();
};
@ -185,22 +243,24 @@ export const SearchExamplesApp = ({
};
const onServerClickHandler = async () => {
if (!indexPattern || !selectedField) return;
if (!indexPattern || !selectedNumericField) return;
try {
const response = await http.get(SERVER_SEARCH_ROUTE_PATH, {
const res = await http.get(SERVER_SEARCH_ROUTE_PATH, {
query: {
index: indexPattern.title,
field: selectedField.name,
field: selectedNumericField!.name,
},
});
notifications.toasts.addSuccess(`Server returned ${JSON.stringify(response)}`);
notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`);
} catch (e) {
notifications.toasts.addDanger('Failed to run search');
}
};
if (!indexPattern) return null;
const onSearchSourceClickHandler = () => {
doSearchSourceSearch();
};
return (
<Router basename={basename}>
@ -212,7 +272,7 @@ export const SearchExamplesApp = ({
useDefaultBehaviors={true}
indexPatterns={indexPattern ? [indexPattern] : undefined}
/>
<EuiPage restrictWidth="1000px">
<EuiPage>
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
@ -227,106 +287,178 @@ export const SearchExamplesApp = ({
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentBody>
<EuiText>
<EuiFlexGrid columns={1}>
<EuiFlexItem>
<EuiFormLabel>Index Pattern</EuiFormLabel>
<IndexPatternSelect
placeholder={i18n.translate(
'backgroundSessionExample.selectIndexPatternPlaceholder',
{
defaultMessage: 'Select index pattern',
}
)}
indexPatternId={indexPattern?.id || ''}
onChange={async (newIndexPatternId: any) => {
const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);
setIndexPattern(newIndexPattern);
}}
isClearable={false}
<EuiFlexGrid columns={3}>
<EuiFlexItem style={{ width: '40%' }}>
<EuiText>
<EuiFlexGrid columns={2}>
<EuiFlexItem>
<EuiFormLabel>Index Pattern</EuiFormLabel>
<IndexPatternSelect
placeholder={i18n.translate(
'backgroundSessionExample.selectIndexPatternPlaceholder',
{
defaultMessage: 'Select index pattern',
}
)}
indexPatternId={indexPattern?.id || ''}
onChange={async (newIndexPatternId: any) => {
const newIndexPattern = await data.indexPatterns.get(
newIndexPatternId
);
setIndexPattern(newIndexPattern);
}}
isClearable={false}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormLabel>Numeric Field to Aggregate</EuiFormLabel>
<EuiComboBox
options={formatFieldsToComboBox(getNumeric(fields))}
selectedOptions={formatFieldToComboBox(selectedNumericField)}
singleSelection={true}
onChange={(option) => {
const fld = indexPattern?.getFieldByName(option[0].label);
setSelectedNumericField(fld || null);
}}
sortMatchesBy="startsWith"
/>
</EuiFlexItem>
</EuiFlexGrid>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormLabel>
Fields to query (leave blank to include all fields)
</EuiFormLabel>
<EuiComboBox
options={formatFieldsToComboBox(fields)}
selectedOptions={formatFieldsToComboBox(selectedFields)}
singleSelection={false}
onChange={(option) => {
const flds = option
.map((opt) => indexPattern?.getFieldByName(opt?.label))
.filter((f) => f);
setSelectedFields(flds.length ? (flds as IndexPatternField[]) : []);
}}
sortMatchesBy="startsWith"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>
Searching Elasticsearch using <EuiCode>data.search</EuiCode>
</h3>
</EuiTitle>
<EuiText>
If you want to fetch data from Elasticsearch, you can use the different
services provided by the <EuiCode>data</EuiCode> plugin. These help you get
the index pattern and search bar configuration, format them into a DSL query
and send it to Elasticsearch.
<EuiSpacer />
<EuiButtonEmpty size="xs" onClick={onClickHandler} iconType="play">
<FormattedMessage
id="searchExamples.buttonText"
defaultMessage="Request from low-level client (data.search.search)"
/>
</EuiButtonEmpty>
<EuiButtonEmpty
size="xs"
onClick={onSearchSourceClickHandler}
iconType="play"
>
<FormattedMessage
id="searchExamples.searchSource.buttonText"
defaultMessage="Request from high-level client (data.search.searchSource)"
/>
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Writing a custom search strategy</h3>
</EuiTitle>
<EuiText>
If you want to do some pre or post processing on the server, you might want
to create a custom search strategy. This example uses such a strategy,
passing in custom input and receiving custom output back.
<EuiSpacer />
<EuiCheckbox
id="GetCool"
label={
<FormattedMessage
id="searchExamples.getCoolCheckbox"
defaultMessage="Get cool parameter?"
/>
}
checked={getCool}
onChange={(event) => setGetCool(event.target.checked)}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormLabel>Numeric Fields</EuiFormLabel>
<EuiComboBox
options={formatFieldsToComboBox(numericFields)}
selectedOptions={formatFieldToComboBox(selectedField)}
singleSelection={true}
onChange={(option) => {
const field = indexPattern.getFieldByName(option[0].label);
setSelectedField(field || null);
}}
sortMatchesBy="startsWith"
/>
</EuiFlexItem>
</EuiFlexGrid>
</EuiText>
<EuiText>
<FormattedMessage
id="searchExamples.timestampText"
defaultMessage="Last query took: {time} ms"
values={{ time: timeTook || 'Unknown' }}
/>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>
Searching Elasticsearch using <EuiCode>data.search</EuiCode>
</h3>
</EuiTitle>
<EuiText>
If you want to fetch data from Elasticsearch, you can use the different services
provided by the <EuiCode>data</EuiCode> plugin. These help you get the index
pattern and search bar configuration, format them into a DSL query and send it
to Elasticsearch.
<EuiSpacer />
<EuiButton type="primary" size="s" onClick={onClickHandler}>
<FormattedMessage id="searchExamples.buttonText" defaultMessage="Get data" />
</EuiButton>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Writing a custom search strategy</h3>
</EuiTitle>
<EuiText>
If you want to do some pre or post processing on the server, you might want to
create a custom search strategy. This example uses such a strategy, passing in
custom input and receiving custom output back.
<EuiSpacer />
<EuiCheckbox
id="GetCool"
label={
<EuiButtonEmpty
size="xs"
onClick={onMyStrategyClickHandler}
iconType="play"
>
<FormattedMessage
id="searchExamples.myStrategyButtonText"
defaultMessage="Request from low-level client via My Strategy"
/>
</EuiButtonEmpty>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Using search on the server</h3>
</EuiTitle>
<EuiText>
You can also run your search request from the server, without registering a
search strategy. This request does not take the configuration of{' '}
<EuiCode>TopNavMenu</EuiCode> into account, but you could pass those down to
the server as well.
<EuiSpacer />
<EuiButtonEmpty size="xs" onClick={onServerClickHandler} iconType="play">
<FormattedMessage
id="searchExamples.myServerButtonText"
defaultMessage="Request from low-level client on the server"
/>
</EuiButtonEmpty>
</EuiText>
</EuiFlexItem>
<EuiFlexItem style={{ width: '30%' }}>
<EuiTitle size="xs">
<h4>Request</h4>
</EuiTitle>
<EuiText size="xs">Search body sent to ES</EuiText>
<EuiCodeBlock
language="json"
fontSize="s"
paddingSize="s"
overflowHeight={450}
isCopyable
>
{JSON.stringify(request, null, 2)}
</EuiCodeBlock>
</EuiFlexItem>
<EuiFlexItem style={{ width: '30%' }}>
<EuiTitle size="xs">
<h4>Response</h4>
</EuiTitle>
<EuiText size="xs">
<FormattedMessage
id="searchExamples.getCoolCheckbox"
defaultMessage="Get cool parameter?"
id="searchExamples.timestampText"
defaultMessage="Took: {time} ms"
values={{ time: timeTook || 'Unknown' }}
/>
}
checked={getCool}
onChange={(event) => setGetCool(event.target.checked)}
/>
<EuiButton type="primary" size="s" onClick={onMyStrategyClickHandler}>
<FormattedMessage
id="searchExamples.myStrategyButtonText"
defaultMessage="Get data via My Strategy"
/>
</EuiButton>
</EuiText>
<EuiSpacer />
<EuiTitle size="s">
<h3>Using search on the server</h3>
</EuiTitle>
<EuiText>
You can also run your search request from the server, without registering a
search strategy. This request does not take the configuration of{' '}
<EuiCode>TopNavMenu</EuiCode> into account, but you could pass those down to the
server as well.
<EuiButton type="primary" size="s" onClick={onServerClickHandler}>
<FormattedMessage
id="searchExamples.myServerButtonText"
defaultMessage="Get data on the server"
/>
</EuiButton>
</EuiText>
</EuiText>
<EuiCodeBlock
language="json"
fontSize="s"
paddingSize="s"
overflowHeight={450}
isCopyable
>
{JSON.stringify(response, null, 2)}
</EuiCodeBlock>
</EuiFlexItem>
</EuiFlexGrid>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>

View file

@ -65,7 +65,7 @@
"kbn:watch": "node scripts/kibana --dev --logging.json=false",
"build:types": "rm -rf ./target/types && tsc --p tsconfig.types.json",
"docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept",
"kbn:bootstrap": "node scripts/build_ts_refs && node scripts/register_git_hook",
"kbn:bootstrap": "node scripts/build_ts_refs",
"spec_to_console": "node scripts/spec_to_console",
"backport-skip-ci": "backport --prDescription \"[skip-ci]\"",
"storybook": "node scripts/storybook",
@ -107,13 +107,15 @@
"@elastic/datemath": "link:packages/elastic-datemath",
"@elastic/elasticsearch": "7.10.0",
"@elastic/ems-client": "7.11.0",
"@elastic/eui": "30.2.0",
"@elastic/eui": "30.5.1",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "^9.0.1-kibana3",
"@elastic/node-crypto": "1.2.1",
"@elastic/numeral": "^2.5.0",
"@elastic/react-search-ui": "^1.5.0",
"@elastic/request-crypto": "1.1.4",
"@elastic/safer-lodash-set": "link:packages/elastic-safer-lodash-set",
"@elastic/search-ui-app-search-connector": "^1.5.0",
"@hapi/boom": "^7.4.11",
"@hapi/cookie": "^10.1.2",
"@hapi/good-squeeze": "5.2.1",
@ -746,7 +748,7 @@
"murmurhash3js": "3.0.1",
"mutation-observer": "^1.0.3",
"ncp": "^2.0.0",
"node-sass": "^4.13.1",
"node-sass": "^4.14.1",
"null-loader": "^3.0.0",
"nyc": "^15.0.1",
"oboe": "^2.1.4",

View file

@ -18,6 +18,6 @@
*/
export { ReportHTTP, Reporter, ReporterConfig } from './reporter';
export { UiStatsMetricType, METRIC_TYPE } from './metrics';
export { UiCounterMetricType, METRIC_TYPE } from './metrics';
export { Report, ReportManager } from './report';
export { Storage } from './storage';

View file

@ -17,16 +17,15 @@
* under the License.
*/
import { UiStatsMetric } from './ui_stats';
import { UiCounterMetric } from './ui_counter';
import { UserAgentMetric } from './user_agent';
import { ApplicationUsageCurrent } from './application_usage';
export { UiStatsMetric, createUiStatsMetric, UiStatsMetricType } from './ui_stats';
export { Stats } from './stats';
export { UiCounterMetric, createUiCounterMetric, UiCounterMetricType } from './ui_counter';
export { trackUsageAgent } from './user_agent';
export { ApplicationUsage, ApplicationUsageCurrent } from './application_usage';
export type Metric = UiStatsMetric | UserAgentMetric | ApplicationUsageCurrent;
export type Metric = UiCounterMetric | UserAgentMetric | ApplicationUsageCurrent;
export enum METRIC_TYPE {
COUNT = 'count',
LOADED = 'loaded',

View file

@ -19,27 +19,27 @@
import { METRIC_TYPE } from './';
export type UiStatsMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT;
export interface UiStatsMetricConfig {
type: UiStatsMetricType;
export type UiCounterMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT;
export interface UiCounterMetricConfig {
type: UiCounterMetricType;
appName: string;
eventName: string;
count?: number;
}
export interface UiStatsMetric {
type: UiStatsMetricType;
export interface UiCounterMetric {
type: UiCounterMetricType;
appName: string;
eventName: string;
count: number;
}
export function createUiStatsMetric({
export function createUiCounterMetric({
type,
appName,
eventName,
count = 1,
}: UiStatsMetricConfig): UiStatsMetric {
}: UiCounterMetricConfig): UiCounterMetric {
return {
type,
appName,

View file

@ -19,19 +19,19 @@
import moment from 'moment-timezone';
import { UnreachableCaseError, wrapArray } from './util';
import { Metric, Stats, UiStatsMetricType, METRIC_TYPE } from './metrics';
const REPORT_VERSION = 1;
import { Metric, UiCounterMetricType, METRIC_TYPE } from './metrics';
const REPORT_VERSION = 2;
export interface Report {
reportVersion: typeof REPORT_VERSION;
uiStatsMetrics?: Record<
uiCounter?: Record<
string,
{
key: string;
appName: string;
eventName: string;
type: UiStatsMetricType;
stats: Stats;
type: UiCounterMetricType;
total: number;
}
>;
userAgent?: Record<
@ -65,25 +65,15 @@ export class ReportManager {
this.report = ReportManager.createReport();
}
public isReportEmpty(): boolean {
const { uiStatsMetrics, userAgent, application_usage: appUsage } = this.report;
const noUiStats = !uiStatsMetrics || Object.keys(uiStatsMetrics).length === 0;
const noUserAgent = !userAgent || Object.keys(userAgent).length === 0;
const { uiCounter, userAgent, application_usage: appUsage } = this.report;
const noUiCounters = !uiCounter || Object.keys(uiCounter).length === 0;
const noUserAgents = !userAgent || Object.keys(userAgent).length === 0;
const noAppUsage = !appUsage || Object.keys(appUsage).length === 0;
return noUiStats && noUserAgent && noAppUsage;
return noUiCounters && noUserAgents && noAppUsage;
}
private incrementStats(count: number, stats?: Stats): Stats {
const { min = 0, max = 0, sum = 0 } = stats || {};
const newMin = Math.min(min, count);
const newMax = Math.max(max, count);
const newAvg = newMin + newMax / 2;
const newSum = sum + count;
return {
min: newMin,
max: newMax,
avg: newAvg,
sum: newSum,
};
private incrementTotal(count: number, currentTotal?: number): number {
const currentTotalNumber = typeof currentTotal === 'number' ? currentTotal : 0;
return count + currentTotalNumber;
}
assignReports(newMetrics: Metric | Metric[]) {
wrapArray(newMetrics).forEach((newMetric) => this.assignReport(this.report, newMetric));
@ -129,14 +119,14 @@ export class ReportManager {
case METRIC_TYPE.LOADED:
case METRIC_TYPE.COUNT: {
const { appName, type, eventName, count } = metric;
report.uiStatsMetrics = report.uiStatsMetrics || {};
const existingStats = (report.uiStatsMetrics[key] || {}).stats;
report.uiStatsMetrics[key] = {
report.uiCounter = report.uiCounter || {};
const currentTotal = report.uiCounter[key]?.total;
report.uiCounter[key] = {
key,
appName,
eventName,
type,
stats: this.incrementStats(count, existingStats),
total: this.incrementTotal(count, currentTotal),
};
return;
}

View file

@ -18,7 +18,7 @@
*/
import { wrapArray } from './util';
import { Metric, createUiStatsMetric, trackUsageAgent, UiStatsMetricType } from './metrics';
import { Metric, createUiCounterMetric, trackUsageAgent, UiCounterMetricType } from './metrics';
import { Storage, ReportStorageManager } from './storage';
import { Report, ReportManager } from './report';
@ -109,15 +109,15 @@ export class Reporter {
}
}
public reportUiStats = (
public reportUiCounter = (
appName: string,
type: UiStatsMetricType,
type: UiCounterMetricType,
eventNames: string | string[],
count?: number
) => {
const metrics = wrapArray(eventNames).map((eventName) => {
this.log(`${type} Metric -> (${appName}:${eventName}):`);
const report = createUiStatsMetric({ type, appName, eventName, count });
const report = createUiCounterMetric({ type, appName, eventName, count });
this.log(report);
return report;
});

View file

@ -27,22 +27,27 @@ import { ApmAgentConfig } from './types';
const getDefaultConfig = (isDistributable: boolean): ApmAgentConfig => {
// https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html
return {
active: process.env.ELASTIC_APM_ACTIVE || false,
active: process.env.ELASTIC_APM_ACTIVE === 'true' || false,
environment: process.env.ELASTIC_APM_ENVIRONMENT || process.env.NODE_ENV || 'development',
serverUrl: 'https://b1e3b4b4233e44cdad468c127d0af8d8.apm.europe-west1.gcp.cloud.es.io:443',
serverUrl: 'https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io',
// The secretToken below is intended to be hardcoded in this file even though
// it makes it public. This is not a security/privacy issue. Normally we'd
// instead disable the need for a secretToken in the APM Server config where
// the data is transmitted to, but due to how it's being hosted, it's easier,
// for now, to simply leave it in.
secretToken: '2OyjjaI6RVkzx2O5CV',
secretToken: 'ZQHYvrmXEx04ozge8F',
logUncaughtExceptions: true,
globalLabels: {},
centralConfig: false,
metricsInterval: isDistributable ? '120s' : '30s',
transactionSampleRate: process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE
? parseFloat(process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE)
: 1.0,
// Can be performance intensive, disabling by default
breakdownMetrics: isDistributable ? false : true,
@ -150,8 +155,9 @@ export class ApmConfiguration {
globalLabels: {
branch: process.env.ghprbSourceBranch || '',
targetBranch: process.env.ghprbTargetBranch || '',
ciJobName: process.env.JOB_NAME || '',
ciBuildNumber: process.env.BUILD_NUMBER || '',
isPr: process.env.GITHUB_PR_NUMBER ? true : false,
prId: process.env.GITHUB_PR_NUMBER || '',
},
};
}

View file

@ -4753,12 +4753,11 @@ exports[`Header renders 1`] = `
hasArrow={true}
id="headerHelpMenu"
isOpen={false}
ownFocus={true}
ownFocus={false}
panelPaddingSize="m"
repositionOnScroll={true}
>
<EuiOutsideClickDetector
isDisabled={true}
onOutsideClick={[Function]}
>
<div

View file

@ -319,7 +319,6 @@ class HeaderHelpMenuUI extends Component<Props, State> {
data-test-subj="helpMenuButton"
id="headerHelpMenu"
isOpen={this.state.isOpen}
ownFocus
repositionOnScroll
>
<EuiPopoverTitle>

View file

@ -38,7 +38,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiStatsMetricType } from '@kbn/analytics';
import { UiCounterMetricType } from '@kbn/analytics';
import { UnregisterCallback } from 'history';
import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types';
@ -1434,7 +1434,7 @@ export interface UiSettingsParams<T = unknown> {
description?: string;
// @deprecated
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
name?: string;

View file

@ -0,0 +1,24 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/** @internal */
export const CORE_USAGE_STATS_TYPE = 'core-usage-stats';
/** @internal */
export const CORE_USAGE_STATS_ID = 'core-usage-stats';

View file

@ -20,7 +20,16 @@
import { PublicMethodsOf } from '@kbn/utility-types';
import { BehaviorSubject } from 'rxjs';
import { CoreUsageDataService } from './core_usage_data_service';
import { CoreUsageData, CoreUsageDataStart } from './types';
import { coreUsageStatsClientMock } from './core_usage_stats_client.mock';
import { CoreUsageData, CoreUsageDataSetup, CoreUsageDataStart } from './types';
const createSetupContractMock = (usageStatsClient = coreUsageStatsClientMock.create()) => {
const setupContract: jest.Mocked<CoreUsageDataSetup> = {
registerType: jest.fn(),
getClient: jest.fn().mockReturnValue(usageStatsClient),
};
return setupContract;
};
const createStartContractMock = () => {
const startContract: jest.Mocked<CoreUsageDataStart> = {
@ -140,7 +149,7 @@ const createStartContractMock = () => {
const createMock = () => {
const mocked: jest.Mocked<PublicMethodsOf<CoreUsageDataService>> = {
setup: jest.fn(),
setup: jest.fn().mockReturnValue(createSetupContractMock()),
start: jest.fn().mockReturnValue(createStartContractMock()),
stop: jest.fn(),
};
@ -149,5 +158,6 @@ const createMock = () => {
export const coreUsageDataServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -34,6 +34,9 @@ import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.
import { CoreUsageDataService } from './core_usage_data_service';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
import { typeRegistryMock } from '../saved_objects/saved_objects_type_registry.mock';
import { CORE_USAGE_STATS_TYPE } from './constants';
import { CoreUsageStatsClient } from './core_usage_stats_client';
describe('CoreUsageDataService', () => {
const getTestScheduler = () =>
@ -63,11 +66,67 @@ describe('CoreUsageDataService', () => {
service = new CoreUsageDataService(coreContext);
});
describe('setup', () => {
it('creates internal repository', async () => {
const metrics = metricsServiceMock.createInternalSetupContract();
const savedObjectsStartPromise = Promise.resolve(
savedObjectsServiceMock.createStartContract()
);
service.setup({ metrics, savedObjectsStartPromise });
const savedObjects = await savedObjectsStartPromise;
expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1);
expect(savedObjects.createInternalRepository).toHaveBeenCalledWith([CORE_USAGE_STATS_TYPE]);
});
describe('#registerType', () => {
it('registers core usage stats type', async () => {
const metrics = metricsServiceMock.createInternalSetupContract();
const savedObjectsStartPromise = Promise.resolve(
savedObjectsServiceMock.createStartContract()
);
const coreUsageData = service.setup({
metrics,
savedObjectsStartPromise,
});
const typeRegistry = typeRegistryMock.create();
coreUsageData.registerType(typeRegistry);
expect(typeRegistry.registerType).toHaveBeenCalledTimes(1);
expect(typeRegistry.registerType).toHaveBeenCalledWith({
name: CORE_USAGE_STATS_TYPE,
hidden: true,
namespaceType: 'agnostic',
mappings: expect.anything(),
});
});
});
describe('#getClient', () => {
it('returns client', async () => {
const metrics = metricsServiceMock.createInternalSetupContract();
const savedObjectsStartPromise = Promise.resolve(
savedObjectsServiceMock.createStartContract()
);
const coreUsageData = service.setup({
metrics,
savedObjectsStartPromise,
});
const usageStatsClient = coreUsageData.getClient();
expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient);
});
});
});
describe('start', () => {
describe('getCoreUsageData', () => {
it('returns core metrics for default config', () => {
it('returns core metrics for default config', async () => {
const metrics = metricsServiceMock.createInternalSetupContract();
service.setup({ metrics });
const savedObjectsStartPromise = Promise.resolve(
savedObjectsServiceMock.createStartContract()
);
service.setup({ metrics, savedObjectsStartPromise });
const elasticsearch = elasticsearchServiceMock.createStart();
elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({
body: [
@ -243,8 +302,11 @@ describe('CoreUsageDataService', () => {
observables.push(newObservable);
return newObservable as Observable<any>;
});
const savedObjectsStartPromise = Promise.resolve(
savedObjectsServiceMock.createStartContract()
);
service.setup({ metrics });
service.setup({ metrics, savedObjectsStartPromise });
// Use the stopTimer$ to delay calling stop() until the third frame
const stopTimer$ = cold('---a|');

View file

@ -21,20 +21,29 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { CoreService } from 'src/core/types';
import { SavedObjectsServiceStart } from 'src/core/server';
import { Logger, SavedObjectsServiceStart, SavedObjectTypeRegistry } from 'src/core/server';
import { CoreContext } from '../core_context';
import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config';
import { HttpConfigType } from '../http';
import { LoggingConfigType } from '../logging';
import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config';
import { CoreServicesUsageData, CoreUsageData, CoreUsageDataStart } from './types';
import {
CoreServicesUsageData,
CoreUsageData,
CoreUsageDataStart,
CoreUsageDataSetup,
} from './types';
import { isConfigured } from './is_configured';
import { ElasticsearchServiceStart } from '../elasticsearch';
import { KibanaConfigType } from '../kibana_config';
import { coreUsageStatsType } from './core_usage_stats';
import { CORE_USAGE_STATS_TYPE } from './constants';
import { CoreUsageStatsClient } from './core_usage_stats_client';
import { MetricsServiceSetup, OpsMetrics } from '..';
export interface SetupDeps {
metrics: MetricsServiceSetup;
savedObjectsStartPromise: Promise<SavedObjectsServiceStart>;
}
export interface StartDeps {
@ -60,7 +69,8 @@ const kibanaOrTaskManagerIndex = (index: string, kibanaConfigIndex: string) => {
return index === kibanaConfigIndex ? '.kibana' : '.kibana_task_manager';
};
export class CoreUsageDataService implements CoreService<void, CoreUsageDataStart> {
export class CoreUsageDataService implements CoreService<CoreUsageDataSetup, CoreUsageDataStart> {
private logger: Logger;
private elasticsearchConfig?: ElasticsearchConfigType;
private configService: CoreContext['configService'];
private httpConfig?: HttpConfigType;
@ -69,8 +79,10 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
private stop$: Subject<void>;
private opsMetrics?: OpsMetrics;
private kibanaConfig?: KibanaConfigType;
private coreUsageStatsClient?: CoreUsageStatsClient;
constructor(core: CoreContext) {
this.logger = core.logger.get('core-usage-stats-service');
this.configService = core.configService;
this.stop$ = new Subject();
}
@ -130,8 +142,15 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
throw new Error('Unable to read config values. Ensure that setup() has completed.');
}
if (!this.coreUsageStatsClient) {
throw new Error(
'Core usage stats client is not initialized. Ensure that setup() has completed.'
);
}
const es = this.elasticsearchConfig;
const soUsageData = await this.getSavedObjectIndicesUsageData(savedObjects, elasticsearch);
const coreUsageStatsData = await this.coreUsageStatsClient.getUsageStats();
const http = this.httpConfig;
return {
@ -225,10 +244,11 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
services: {
savedObjects: soUsageData,
},
...coreUsageStatsData,
};
}
setup({ metrics }: SetupDeps) {
setup({ metrics, savedObjectsStartPromise }: SetupDeps) {
metrics
.getOpsMetrics$()
.pipe(takeUntil(this.stop$))
@ -268,6 +288,24 @@ export class CoreUsageDataService implements CoreService<void, CoreUsageDataStar
.subscribe((config) => {
this.kibanaConfig = config;
});
const internalRepositoryPromise = savedObjectsStartPromise.then((savedObjects) =>
savedObjects.createInternalRepository([CORE_USAGE_STATS_TYPE])
);
const registerType = (typeRegistry: SavedObjectTypeRegistry) => {
typeRegistry.registerType(coreUsageStatsType);
};
const getClient = () => {
const debugLogger = (message: string) => this.logger.debug(message);
return new CoreUsageStatsClient(debugLogger, internalRepositoryPromise);
};
this.coreUsageStatsClient = getClient();
return { registerType, getClient } as CoreUsageDataSetup;
}
start({ savedObjects, elasticsearch }: StartDeps) {

View file

@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SavedObjectsType } from '../saved_objects';
import { CORE_USAGE_STATS_TYPE } from './constants';
/** @internal */
export const coreUsageStatsType: SavedObjectsType = {
name: CORE_USAGE_STATS_TYPE,
hidden: true,
namespaceType: 'agnostic',
mappings: {
dynamic: false, // we aren't querying or aggregating over this data, so we don't need to specify any fields
properties: {},
},
};

View file

@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CoreUsageStatsClient } from '.';
const createUsageStatsClientMock = () =>
(({
getUsageStats: jest.fn().mockResolvedValue({}),
incrementSavedObjectsImport: jest.fn().mockResolvedValue(null),
incrementSavedObjectsResolveImportErrors: jest.fn().mockResolvedValue(null),
incrementSavedObjectsExport: jest.fn().mockResolvedValue(null),
} as unknown) as jest.Mocked<CoreUsageStatsClient>);
export const coreUsageStatsClientMock = {
create: createUsageStatsClientMock,
};

View file

@ -0,0 +1,227 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { savedObjectsRepositoryMock } from '../mocks';
import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants';
import {
IncrementSavedObjectsImportOptions,
IncrementSavedObjectsResolveImportErrorsOptions,
IncrementSavedObjectsExportOptions,
IMPORT_STATS_PREFIX,
RESOLVE_IMPORT_STATS_PREFIX,
EXPORT_STATS_PREFIX,
} from './core_usage_stats_client';
import { CoreUsageStatsClient } from '.';
describe('CoreUsageStatsClient', () => {
const setup = () => {
const debugLoggerMock = jest.fn();
const repositoryMock = savedObjectsRepositoryMock.create();
const usageStatsClient = new CoreUsageStatsClient(
debugLoggerMock,
Promise.resolve(repositoryMock)
);
return { usageStatsClient, debugLoggerMock, repositoryMock };
};
const firstPartyRequestHeaders = { 'kbn-version': 'a', origin: 'b', referer: 'c' }; // as long as these three header fields are truthy, this will be treated like a first-party request
const incrementOptions = { refresh: false };
describe('#getUsageStats', () => {
it('returns empty object when encountering a repository error', async () => {
const { usageStatsClient, repositoryMock } = setup();
repositoryMock.get.mockRejectedValue(new Error('Oh no!'));
const result = await usageStatsClient.getUsageStats();
expect(result).toEqual({});
});
it('returns object attributes when usage stats exist', async () => {
const { usageStatsClient, repositoryMock } = setup();
const usageStats = { foo: 'bar' };
repositoryMock.incrementCounter.mockResolvedValue({
type: CORE_USAGE_STATS_TYPE,
id: CORE_USAGE_STATS_ID,
attributes: usageStats,
references: [],
});
const result = await usageStatsClient.getUsageStats();
expect(result).toEqual(usageStats);
});
});
describe('#incrementSavedObjectsImport', () => {
it('does not throw an error if repository incrementCounter operation fails', async () => {
const { usageStatsClient, repositoryMock } = setup();
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
await expect(
usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions)
).resolves.toBeUndefined();
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
});
it('handles falsy options appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup();
await usageStatsClient.incrementSavedObjectsImport({} as IncrementSavedObjectsImportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${IMPORT_STATS_PREFIX}.total`,
`${IMPORT_STATS_PREFIX}.kibanaRequest.no`,
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
`${IMPORT_STATS_PREFIX}.overwriteEnabled.no`,
],
incrementOptions
);
});
it('handles truthy options appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup();
await usageStatsClient.incrementSavedObjectsImport({
headers: firstPartyRequestHeaders,
createNewCopies: true,
overwrite: true,
} as IncrementSavedObjectsImportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${IMPORT_STATS_PREFIX}.total`,
`${IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
`${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`,
],
incrementOptions
);
});
});
describe('#incrementSavedObjectsResolveImportErrors', () => {
it('does not throw an error if repository incrementCounter operation fails', async () => {
const { usageStatsClient, repositoryMock } = setup();
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
await expect(
usageStatsClient.incrementSavedObjectsResolveImportErrors(
{} as IncrementSavedObjectsResolveImportErrorsOptions
)
).resolves.toBeUndefined();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
it('handles falsy options appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup();
await usageStatsClient.incrementSavedObjectsResolveImportErrors(
{} as IncrementSavedObjectsResolveImportErrorsOptions
);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${RESOLVE_IMPORT_STATS_PREFIX}.total`,
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`,
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
],
incrementOptions
);
});
it('handles truthy options appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup();
await usageStatsClient.incrementSavedObjectsResolveImportErrors({
headers: firstPartyRequestHeaders,
createNewCopies: true,
} as IncrementSavedObjectsResolveImportErrorsOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${RESOLVE_IMPORT_STATS_PREFIX}.total`,
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
],
incrementOptions
);
});
});
describe('#incrementSavedObjectsExport', () => {
it('does not throw an error if repository incrementCounter operation fails', async () => {
const { usageStatsClient, repositoryMock } = setup();
repositoryMock.incrementCounter.mockRejectedValue(new Error('Oh no!'));
await expect(
usageStatsClient.incrementSavedObjectsExport({} as IncrementSavedObjectsExportOptions)
).resolves.toBeUndefined();
expect(repositoryMock.incrementCounter).toHaveBeenCalled();
});
it('handles falsy options appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup();
await usageStatsClient.incrementSavedObjectsExport({
types: undefined,
supportedTypes: ['foo', 'bar'],
} as IncrementSavedObjectsExportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${EXPORT_STATS_PREFIX}.total`,
`${EXPORT_STATS_PREFIX}.kibanaRequest.no`,
`${EXPORT_STATS_PREFIX}.allTypesSelected.no`,
],
incrementOptions
);
});
it('handles truthy options appropriately', async () => {
const { usageStatsClient, repositoryMock } = setup();
await usageStatsClient.incrementSavedObjectsExport({
headers: firstPartyRequestHeaders,
types: ['foo', 'bar'],
supportedTypes: ['foo', 'bar'],
} as IncrementSavedObjectsExportOptions);
expect(repositoryMock.incrementCounter).toHaveBeenCalledTimes(1);
expect(repositoryMock.incrementCounter).toHaveBeenCalledWith(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${EXPORT_STATS_PREFIX}.total`,
`${EXPORT_STATS_PREFIX}.kibanaRequest.yes`,
`${EXPORT_STATS_PREFIX}.allTypesSelected.yes`,
],
incrementOptions
);
});
});
});

View file

@ -0,0 +1,154 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CORE_USAGE_STATS_TYPE, CORE_USAGE_STATS_ID } from './constants';
import { CoreUsageStats } from './types';
import {
Headers,
ISavedObjectsRepository,
SavedObjectsImportOptions,
SavedObjectsResolveImportErrorsOptions,
SavedObjectsExportOptions,
} from '..';
interface BaseIncrementOptions {
headers?: Headers;
}
/** @internal */
export type IncrementSavedObjectsImportOptions = BaseIncrementOptions &
Pick<SavedObjectsImportOptions, 'createNewCopies' | 'overwrite'>;
/** @internal */
export type IncrementSavedObjectsResolveImportErrorsOptions = BaseIncrementOptions &
Pick<SavedObjectsResolveImportErrorsOptions, 'createNewCopies'>;
/** @internal */
export type IncrementSavedObjectsExportOptions = BaseIncrementOptions &
Pick<SavedObjectsExportOptions, 'types'> & { supportedTypes: string[] };
export const IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsImport';
export const RESOLVE_IMPORT_STATS_PREFIX = 'apiCalls.savedObjectsResolveImportErrors';
export const EXPORT_STATS_PREFIX = 'apiCalls.savedObjectsExport';
const ALL_COUNTER_FIELDS = [
`${IMPORT_STATS_PREFIX}.total`,
`${IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
`${IMPORT_STATS_PREFIX}.kibanaRequest.no`,
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
`${IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
`${IMPORT_STATS_PREFIX}.overwriteEnabled.yes`,
`${IMPORT_STATS_PREFIX}.overwriteEnabled.no`,
`${RESOLVE_IMPORT_STATS_PREFIX}.total`,
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.yes`,
`${RESOLVE_IMPORT_STATS_PREFIX}.kibanaRequest.no`,
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.no`,
`${EXPORT_STATS_PREFIX}.total`,
`${EXPORT_STATS_PREFIX}.kibanaRequest.yes`,
`${EXPORT_STATS_PREFIX}.kibanaRequest.no`,
`${EXPORT_STATS_PREFIX}.allTypesSelected.yes`,
`${EXPORT_STATS_PREFIX}.allTypesSelected.no`,
];
/** @internal */
export class CoreUsageStatsClient {
constructor(
private readonly debugLogger: (message: string) => void,
private readonly repositoryPromise: Promise<ISavedObjectsRepository>
) {}
public async getUsageStats() {
this.debugLogger('getUsageStats() called');
let coreUsageStats: CoreUsageStats = {};
try {
const repository = await this.repositoryPromise;
const result = await repository.incrementCounter<CoreUsageStats>(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
ALL_COUNTER_FIELDS,
{ initialize: true } // set all counter fields to 0 if they don't exist
);
coreUsageStats = result.attributes;
} catch (err) {
// do nothing
}
return coreUsageStats;
}
public async incrementSavedObjectsImport({
headers,
createNewCopies,
overwrite,
}: IncrementSavedObjectsImportOptions) {
const isKibanaRequest = getIsKibanaRequest(headers);
const counterFieldNames = [
'total',
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`,
`overwriteEnabled.${overwrite ? 'yes' : 'no'}`,
];
await this.updateUsageStats(counterFieldNames, IMPORT_STATS_PREFIX);
}
public async incrementSavedObjectsResolveImportErrors({
headers,
createNewCopies,
}: IncrementSavedObjectsResolveImportErrorsOptions) {
const isKibanaRequest = getIsKibanaRequest(headers);
const counterFieldNames = [
'total',
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
`createNewCopiesEnabled.${createNewCopies ? 'yes' : 'no'}`,
];
await this.updateUsageStats(counterFieldNames, RESOLVE_IMPORT_STATS_PREFIX);
}
public async incrementSavedObjectsExport({
headers,
types,
supportedTypes,
}: IncrementSavedObjectsExportOptions) {
const isKibanaRequest = getIsKibanaRequest(headers);
const isAllTypesSelected = !!types && supportedTypes.every((x) => types.includes(x));
const counterFieldNames = [
'total',
`kibanaRequest.${isKibanaRequest ? 'yes' : 'no'}`,
`allTypesSelected.${isAllTypesSelected ? 'yes' : 'no'}`,
];
await this.updateUsageStats(counterFieldNames, EXPORT_STATS_PREFIX);
}
private async updateUsageStats(counterFieldNames: string[], prefix: string) {
const options = { refresh: false };
try {
const repository = await this.repositoryPromise;
await repository.incrementCounter(
CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
counterFieldNames.map((x) => `${prefix}.${x}`),
options
);
} catch (err) {
// do nothing
}
}
}
function getIsKibanaRequest(headers?: Headers) {
// The presence of these three request headers gives us a good indication that this is a first-party request from the Kibana client.
// We can't be 100% certain, but this is a reasonable attempt.
return headers && headers['kbn-version'] && headers.origin && headers.referer;
}

View file

@ -16,16 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
export { CoreUsageDataStart } from './types';
export { CoreUsageDataSetup, CoreUsageDataStart } from './types';
export { CoreUsageDataService } from './core_usage_data_service';
export { CoreUsageStatsClient } from './core_usage_stats_client';
// Because of #79265 we need to explicity import, then export these types for
// scripts/telemetry_check.js to work as expected
import {
CoreUsageStats,
CoreUsageData,
CoreConfigUsageData,
CoreEnvironmentUsageData,
CoreServicesUsageData,
} from './types';
export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData };
export {
CoreUsageStats,
CoreUsageData,
CoreConfigUsageData,
CoreEnvironmentUsageData,
CoreServicesUsageData,
};

View file

@ -17,11 +17,40 @@
* under the License.
*/
import { CoreUsageStatsClient } from './core_usage_stats_client';
import { ISavedObjectTypeRegistry, SavedObjectTypeRegistry } from '..';
/**
* @internal
*
* CoreUsageStats are collected over time while Kibana is running. This is related to CoreUsageData, which is a superset of this that also
* includes point-in-time configuration information.
* */
export interface CoreUsageStats {
'apiCalls.savedObjectsImport.total'?: number;
'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number;
'apiCalls.savedObjectsImport.kibanaRequest.no'?: number;
'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number;
'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number;
'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number;
'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number;
'apiCalls.savedObjectsResolveImportErrors.total'?: number;
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number;
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number;
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number;
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number;
'apiCalls.savedObjectsExport.total'?: number;
'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number;
'apiCalls.savedObjectsExport.kibanaRequest.no'?: number;
'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number;
'apiCalls.savedObjectsExport.allTypesSelected.no'?: number;
}
/**
* Type describing Core's usage data payload
* @internal
*/
export interface CoreUsageData {
export interface CoreUsageData extends CoreUsageStats {
config: CoreConfigUsageData;
services: CoreServicesUsageData;
environment: CoreEnvironmentUsageData;
@ -141,6 +170,14 @@ export interface CoreConfigUsageData {
// };
}
/** @internal */
export interface CoreUsageDataSetup {
registerType(
typeRegistry: ISavedObjectTypeRegistry & Pick<SavedObjectTypeRegistry, 'registerType'>
): void;
getClient(): CoreUsageStatsClient;
}
/**
* Internal API for getting Core's usage data payload.
*

View file

@ -69,13 +69,20 @@ import { I18nServiceSetup } from './i18n';
// Because of #79265 we need to explicity import, then export these types for
// scripts/telemetry_check.js to work as expected
import {
CoreUsageStats,
CoreUsageData,
CoreConfigUsageData,
CoreEnvironmentUsageData,
CoreServicesUsageData,
} from './core_usage_data';
export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData };
export {
CoreUsageStats,
CoreUsageData,
CoreConfigUsageData,
CoreEnvironmentUsageData,
CoreServicesUsageData,
};
export { bootstrap } from './bootstrap';
export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities';
@ -295,6 +302,7 @@ export {
SavedObjectsRepository,
SavedObjectsDeleteByNamespaceOptions,
SavedObjectsIncrementCounterOptions,
SavedObjectsIncrementCounterField,
SavedObjectsComplexFieldMapping,
SavedObjectsCoreFieldMapping,
SavedObjectsFieldMapping,

View file

@ -48,6 +48,7 @@ export {
export {
ISavedObjectsRepository,
SavedObjectsIncrementCounterOptions,
SavedObjectsIncrementCounterField,
SavedObjectsDeleteByNamespaceOptions,
} from './service/lib/repository';

View file

@ -22,11 +22,20 @@ import stringify from 'json-stable-stringify';
import { createPromiseFromStreams, createMapStream, createConcatStream } from '@kbn/utils';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { SavedObjectConfig } from '../saved_objects_config';
import { exportSavedObjectsToStream } from '../export';
import { validateTypes, validateObjects } from './utils';
export const registerExportRoute = (router: IRouter, config: SavedObjectConfig) => {
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: CoreUsageDataSetup;
}
export const registerExportRoute = (
router: IRouter,
{ config, coreUsageData }: RouteDependencies
) => {
const { maxImportExportSize } = config;
const referenceSchema = schema.object({
@ -95,6 +104,12 @@ export const registerExportRoute = (router: IRouter, config: SavedObjectConfig)
}
}
const { headers } = req;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient
.incrementSavedObjectsExport({ headers, types, supportedTypes })
.catch(() => {});
const exportStream = await exportSavedObjectsToStream({
savedObjectsClient,
types,

View file

@ -21,17 +21,26 @@ import { Readable } from 'stream';
import { extname } from 'path';
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { importSavedObjectsFromStream } from '../import';
import { SavedObjectConfig } from '../saved_objects_config';
import { createSavedObjectsStreamFromNdJson } from './utils';
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: CoreUsageDataSetup;
}
interface FileStream extends Readable {
hapi: {
filename: string;
};
}
export const registerImportRoute = (router: IRouter, config: SavedObjectConfig) => {
export const registerImportRoute = (
router: IRouter,
{ config, coreUsageData }: RouteDependencies
) => {
const { maxImportExportSize, maxImportPayloadBytes } = config;
router.post(
@ -65,6 +74,13 @@ export const registerImportRoute = (router: IRouter, config: SavedObjectConfig)
},
router.handleLegacyErrors(async (context, req, res) => {
const { overwrite, createNewCopies } = req.query;
const { headers } = req;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient
.incrementSavedObjectsImport({ headers, createNewCopies, overwrite })
.catch(() => {});
const file = req.body.file as FileStream;
const fileExtension = extname(file.hapi.filename).toLowerCase();
if (fileExtension !== '.ndjson') {

View file

@ -18,6 +18,7 @@
*/
import { InternalHttpServiceSetup } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { Logger } from '../../logging';
import { SavedObjectConfig } from '../saved_objects_config';
import { IKibanaMigrator } from '../migrations';
@ -37,11 +38,13 @@ import { registerMigrateRoute } from './migrate';
export function registerRoutes({
http,
coreUsageData,
logger,
config,
migratorPromise,
}: {
http: InternalHttpServiceSetup;
coreUsageData: CoreUsageDataSetup;
logger: Logger;
config: SavedObjectConfig;
migratorPromise: Promise<IKibanaMigrator>;
@ -57,9 +60,9 @@ export function registerRoutes({
registerBulkCreateRoute(router);
registerBulkUpdateRoute(router);
registerLogLegacyImportRoute(router, logger);
registerExportRoute(router, config);
registerImportRoute(router, config);
registerResolveImportErrorsRoute(router, config);
registerExportRoute(router, { config, coreUsageData });
registerImportRoute(router, { config, coreUsageData });
registerResolveImportErrorsRoute(router, { config, coreUsageData });
const internalRouter = http.createRouter('/internal/saved_objects/');

View file

@ -25,6 +25,9 @@ import * as exportMock from '../../export';
import supertest from 'supertest';
import type { UnwrapPromise } from '@kbn/utility-types';
import { createListStream } from '@kbn/utils';
import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { SavedObjectConfig } from '../../saved_objects_config';
import { registerExportRoute } from '../export';
import { setupServer, createExportableType } from '../test_utils';
@ -36,6 +39,7 @@ const config = {
maxImportPayloadBytes: 26214400,
maxImportExportSize: 10000,
} as SavedObjectConfig;
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;
describe('POST /api/saved_objects/_export', () => {
let server: SetupServerReturn['server'];
@ -49,7 +53,10 @@ describe('POST /api/saved_objects/_export', () => {
);
const router = httpSetup.createRouter('/api/saved_objects/');
registerExportRoute(router, config);
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsExport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the export does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
registerExportRoute(router, { config, coreUsageData });
await server.start();
});
@ -59,7 +66,7 @@ describe('POST /api/saved_objects/_export', () => {
await server.stop();
});
it('formats successful response', async () => {
it('formats successful response and records usage stats', async () => {
const sortedObjects = [
{
id: '1',
@ -110,5 +117,10 @@ describe('POST /api/saved_objects/_export', () => {
types: ['search'],
})
);
expect(coreUsageStatsClient.incrementSavedObjectsExport).toHaveBeenCalledWith({
headers: expect.anything(),
types: ['search'],
supportedTypes: ['index-pattern', 'search'],
});
});
});

View file

@ -22,6 +22,9 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerImportRoute } from '../import';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { SavedObjectConfig } from '../../saved_objects_config';
import { setupServer, createExportableType } from '../test_utils';
import { SavedObjectsErrorHelpers } from '../..';
@ -31,6 +34,7 @@ type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
const { v4: uuidv4 } = jest.requireActual('uuid');
const allowedTypes = ['index-pattern', 'visualization', 'dashboard'];
const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig;
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;
const URL = '/internal/saved_objects/_import';
describe(`POST ${URL}`, () => {
@ -71,7 +75,10 @@ describe(`POST ${URL}`, () => {
savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] });
const router = httpSetup.createRouter('/internal/saved_objects/');
registerImportRoute(router, config);
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsImport.mockRejectedValue(new Error('Oh no!')); // this error is intentionally swallowed so the import does not fail
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
registerImportRoute(router, { config, coreUsageData });
await server.start();
});
@ -80,7 +87,7 @@ describe(`POST ${URL}`, () => {
await server.stop();
});
it('formats successful response', async () => {
it('formats successful response and records usage stats', async () => {
const result = await supertest(httpSetup.server.listener)
.post(URL)
.set('content-Type', 'multipart/form-data; boundary=BOUNDARY')
@ -98,6 +105,11 @@ describe(`POST ${URL}`, () => {
expect(result.body).toEqual({ success: true, successCount: 0 });
expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created
expect(coreUsageStatsClient.incrementSavedObjectsImport).toHaveBeenCalledWith({
headers: expect.anything(),
createNewCopies: false,
overwrite: false,
});
});
it('defaults migrationVersion to empty object', async () => {

View file

@ -22,6 +22,9 @@ import supertest from 'supertest';
import { UnwrapPromise } from '@kbn/utility-types';
import { registerResolveImportErrorsRoute } from '../resolve_import_errors';
import { savedObjectsClientMock } from '../../../../../core/server/mocks';
import { CoreUsageStatsClient } from '../../../core_usage_data';
import { coreUsageStatsClientMock } from '../../../core_usage_data/core_usage_stats_client.mock';
import { coreUsageDataServiceMock } from '../../../core_usage_data/core_usage_data_service.mock';
import { setupServer, createExportableType } from '../test_utils';
import { SavedObjectConfig } from '../../saved_objects_config';
@ -30,6 +33,7 @@ type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
const { v4: uuidv4 } = jest.requireActual('uuid');
const allowedTypes = ['index-pattern', 'visualization', 'dashboard'];
const config = { maxImportPayloadBytes: 26214400, maxImportExportSize: 10000 } as SavedObjectConfig;
let coreUsageStatsClient: jest.Mocked<CoreUsageStatsClient>;
const URL = '/api/saved_objects/_resolve_import_errors';
describe(`POST ${URL}`, () => {
@ -76,7 +80,12 @@ describe(`POST ${URL}`, () => {
savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] });
const router = httpSetup.createRouter('/api/saved_objects/');
registerResolveImportErrorsRoute(router, config);
coreUsageStatsClient = coreUsageStatsClientMock.create();
coreUsageStatsClient.incrementSavedObjectsResolveImportErrors.mockRejectedValue(
new Error('Oh no!') // this error is intentionally swallowed so the export does not fail
);
const coreUsageData = coreUsageDataServiceMock.createSetupContract(coreUsageStatsClient);
registerResolveImportErrorsRoute(router, { config, coreUsageData });
await server.start();
});
@ -85,7 +94,7 @@ describe(`POST ${URL}`, () => {
await server.stop();
});
it('formats successful response', async () => {
it('formats successful response and records usage stats', async () => {
const result = await supertest(httpSetup.server.listener)
.post(URL)
.set('content-Type', 'multipart/form-data; boundary=BOUNDARY')
@ -107,6 +116,10 @@ describe(`POST ${URL}`, () => {
expect(result.body).toEqual({ success: true, successCount: 0 });
expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created
expect(coreUsageStatsClient.incrementSavedObjectsResolveImportErrors).toHaveBeenCalledWith({
headers: expect.anything(),
createNewCopies: false,
});
});
it('defaults migrationVersion to empty object', async () => {

View file

@ -21,17 +21,26 @@ import { extname } from 'path';
import { Readable } from 'stream';
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../http';
import { CoreUsageDataSetup } from '../../core_usage_data';
import { resolveSavedObjectsImportErrors } from '../import';
import { SavedObjectConfig } from '../saved_objects_config';
import { createSavedObjectsStreamFromNdJson } from './utils';
interface RouteDependencies {
config: SavedObjectConfig;
coreUsageData: CoreUsageDataSetup;
}
interface FileStream extends Readable {
hapi: {
filename: string;
};
}
export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedObjectConfig) => {
export const registerResolveImportErrorsRoute = (
router: IRouter,
{ config, coreUsageData }: RouteDependencies
) => {
const { maxImportExportSize, maxImportPayloadBytes } = config;
router.post(
@ -72,6 +81,14 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO
},
},
router.handleLegacyErrors(async (context, req, res) => {
const { createNewCopies } = req.query;
const { headers } = req;
const usageStatsClient = coreUsageData.getClient();
usageStatsClient
.incrementSavedObjectsResolveImportErrors({ headers, createNewCopies })
.catch(() => {});
const file = req.body.file as FileStream;
const fileExtension = extname(file.hapi.filename).toLowerCase();
if (fileExtension !== '.ndjson') {
@ -93,7 +110,7 @@ export const registerResolveImportErrorsRoute = (router: IRouter, config: SavedO
readStream,
retries: req.body.retries,
objectLimit: maxImportExportSize,
createNewCopies: req.query.createNewCopies,
createNewCopies,
});
return res.ok({ body: result });

View file

@ -33,6 +33,7 @@ import { Env } from '../config';
import { configServiceMock } from '../mocks';
import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock';
import { elasticsearchClientMock } from '../elasticsearch/client/mocks';
import { coreUsageDataServiceMock } from '../core_usage_data/core_usage_data_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { httpServerMock } from '../http/http_server.mocks';
import { SavedObjectsClientFactoryProvider } from './service/lib';
@ -64,6 +65,7 @@ describe('SavedObjectsService', () => {
return {
http: httpServiceMock.createInternalSetupContract(),
elasticsearch: elasticsearchMock,
coreUsageData: coreUsageDataServiceMock.createSetupContract(),
};
};

View file

@ -27,6 +27,7 @@ import {
} from './';
import { KibanaMigrator, IKibanaMigrator } from './migrations';
import { CoreContext } from '../core_context';
import { CoreUsageDataSetup } from '../core_usage_data';
import {
ElasticsearchClient,
IClusterClient,
@ -253,6 +254,7 @@ export interface SavedObjectsRepositoryFactory {
export interface SavedObjectsSetupDeps {
http: InternalHttpServiceSetup;
elasticsearch: InternalElasticsearchServiceSetup;
coreUsageData: CoreUsageDataSetup;
}
interface WrappedClientFactoryWrapper {
@ -288,6 +290,7 @@ export class SavedObjectsService
this.logger.debug('Setting up SavedObjects service');
this.setupDeps = setupDeps;
const { http, elasticsearch, coreUsageData } = setupDeps;
const savedObjectsConfig = await this.coreContext.configService
.atPath<SavedObjectsConfigType>('savedObjects')
@ -299,8 +302,11 @@ export class SavedObjectsService
.toPromise();
this.config = new SavedObjectConfig(savedObjectsConfig, savedObjectsMigrationConfig);
coreUsageData.registerType(this.typeRegistry);
registerRoutes({
http: setupDeps.http,
http,
coreUsageData,
logger: this.logger,
config: this.config,
migratorPromise: this.migrator$.pipe(first()).toPromise(),
@ -309,7 +315,7 @@ export class SavedObjectsService
return {
status$: calculateStatus$(
this.migrator$.pipe(switchMap((migrator) => migrator.getStatus$())),
setupDeps.elasticsearch.status$
elasticsearch.status$
),
setClientFactoryProvider: (provider) => {
if (this.started) {

View file

@ -3412,11 +3412,13 @@ describe('SavedObjectsRepository', () => {
await test({});
});
it(`throws when counterFieldName is not a string`, async () => {
it(`throws when counterField is not CounterField type`, async () => {
const test = async (field) => {
await expect(
savedObjectsRepository.incrementCounter(type, id, field)
).rejects.toThrowError(`"counterFieldNames" argument must be an array of strings`);
).rejects.toThrowError(
`"counterFields" argument must be of type Array<string | { incrementBy?: number; fieldName: string }>`
);
expect(client.update).not.toHaveBeenCalled();
};
@ -3425,6 +3427,7 @@ describe('SavedObjectsRepository', () => {
await test([false]);
await test([{}]);
await test([{}, false, 42, null, 'string']);
await test([{ fieldName: 'string' }, false, null, 'string']);
});
it(`throws when type is invalid`, async () => {
@ -3513,6 +3516,25 @@ describe('SavedObjectsRepository', () => {
originId,
});
});
it('increments counter by incrementBy config', async () => {
await incrementCounterSuccess(type, id, [{ fieldName: counterFields[0], incrementBy: 3 }]);
expect(client.update).toBeCalledTimes(1);
expect(client.update).toBeCalledWith(
expect.objectContaining({
body: expect.objectContaining({
script: expect.objectContaining({
params: expect.objectContaining({
counterFieldNames: [counterFields[0]],
counts: [3],
}),
}),
}),
}),
expect.anything()
);
});
});
});

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { omit } from 'lodash';
import { omit, isObject } from 'lodash';
import uuid from 'uuid';
import {
ElasticsearchClient,
@ -133,6 +133,16 @@ const DEFAULT_REFRESH_SETTING = 'wait_for';
*/
export type ISavedObjectsRepository = Pick<SavedObjectsRepository, keyof SavedObjectsRepository>;
/**
* @public
*/
export interface SavedObjectsIncrementCounterField {
/** The field name to increment the counter by.*/
fieldName: string;
/** The number to increment the field by (defaults to 1).*/
incrementBy?: number;
}
/**
* @public
*/
@ -1524,7 +1534,7 @@ export class SavedObjectsRepository {
}
/**
* Increments all the specified counter fields by one. Creates the document
* Increments all the specified counter fields (by one by default). Creates the document
* if one doesn't exist for the given id.
*
* @remarks
@ -1558,30 +1568,47 @@ export class SavedObjectsRepository {
*
* @param type - The type of saved object whose fields should be incremented
* @param id - The id of the document whose fields should be incremented
* @param counterFieldNames - An array of field names to increment
* @param counterFields - An array of field names to increment or an array of {@link SavedObjectsIncrementCounterField}
* @param options - {@link SavedObjectsIncrementCounterOptions}
* @returns The saved object after the specified fields were incremented
*/
async incrementCounter(
async incrementCounter<T = unknown>(
type: string,
id: string,
counterFieldNames: string[],
counterFields: Array<string | SavedObjectsIncrementCounterField>,
options: SavedObjectsIncrementCounterOptions = {}
): Promise<SavedObject> {
): Promise<SavedObject<T>> {
if (typeof type !== 'string') {
throw new Error('"type" argument must be a string');
}
const isArrayOfStrings =
Array.isArray(counterFieldNames) &&
!counterFieldNames.some((field) => typeof field !== 'string');
if (!isArrayOfStrings) {
throw new Error('"counterFieldNames" argument must be an array of strings');
const isArrayOfCounterFields =
Array.isArray(counterFields) &&
counterFields.every(
(field) =>
typeof field === 'string' || (isObject(field) && typeof field.fieldName === 'string')
);
if (!isArrayOfCounterFields) {
throw new Error(
'"counterFields" argument must be of type Array<string | { incrementBy?: number; fieldName: string }>'
);
}
if (!this._allowedTypes.includes(type)) {
throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
}
const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING, initialize = false } = options;
const normalizedCounterFields = counterFields.map((counterField) => {
const fieldName = typeof counterField === 'string' ? counterField : counterField.fieldName;
const incrementBy = typeof counterField === 'string' ? 1 : counterField.incrementBy || 1;
return {
fieldName,
incrementBy: initialize ? 0 : incrementBy,
};
});
const namespace = normalizeNamespace(options.namespace);
const time = this._getCurrentTime();
@ -1594,13 +1621,15 @@ export class SavedObjectsRepository {
savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace);
}
// attributes: { [counterFieldName]: incrementBy },
const migrated = this._migrator.migrateDocument({
id,
type,
...(savedObjectNamespace && { namespace: savedObjectNamespace }),
...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }),
attributes: counterFieldNames.reduce((acc, counterFieldName) => {
acc[counterFieldName] = initialize ? 0 : 1;
attributes: normalizedCounterFields.reduce((acc, counterField) => {
const { fieldName, incrementBy } = counterField;
acc[fieldName] = incrementBy;
return acc;
}, {} as Record<string, number>),
migrationVersion,
@ -1617,22 +1646,29 @@ export class SavedObjectsRepository {
body: {
script: {
source: `
for (counterFieldName in params.counterFieldNames) {
for (int i = 0; i < params.counterFieldNames.length; i++) {
def counterFieldName = params.counterFieldNames[i];
def count = params.counts[i];
if (ctx._source[params.type][counterFieldName] == null) {
ctx._source[params.type][counterFieldName] = params.count;
ctx._source[params.type][counterFieldName] = count;
}
else {
ctx._source[params.type][counterFieldName] += params.count;
ctx._source[params.type][counterFieldName] += count;
}
}
ctx._source.updated_at = params.time;
`,
lang: 'painless',
params: {
count: initialize ? 0 : 1,
counts: normalizedCounterFields.map(
(normalizedCounterField) => normalizedCounterField.incrementBy
),
counterFieldNames: normalizedCounterFields.map(
(normalizedCounterField) => normalizedCounterField.fieldName
),
time,
type,
counterFieldNames,
},
},
upsert: raw._source,

View file

@ -160,7 +160,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
import { Type } from '@kbn/config-schema';
import { TypeOf } from '@kbn/config-schema';
import { UiStatsMetricType } from '@kbn/analytics';
import { UiCounterMetricType } from '@kbn/analytics';
import { UpdateDocumentByQueryParams } from 'elasticsearch';
import { UpdateDocumentParams } from 'elasticsearch';
import { URL } from 'url';
@ -521,7 +521,7 @@ export interface CoreStatus {
}
// @internal
export interface CoreUsageData {
export interface CoreUsageData extends CoreUsageStats {
// (undocumented)
config: CoreConfigUsageData;
// (undocumented)
@ -535,6 +535,44 @@ export interface CoreUsageDataStart {
getCoreUsageData(): Promise<CoreUsageData>;
}
// @internal
export interface CoreUsageStats {
// (undocumented)
'apiCalls.savedObjectsExport.allTypesSelected.no'?: number;
// (undocumented)
'apiCalls.savedObjectsExport.allTypesSelected.yes'?: number;
// (undocumented)
'apiCalls.savedObjectsExport.kibanaRequest.no'?: number;
// (undocumented)
'apiCalls.savedObjectsExport.kibanaRequest.yes'?: number;
// (undocumented)
'apiCalls.savedObjectsExport.total'?: number;
// (undocumented)
'apiCalls.savedObjectsImport.createNewCopiesEnabled.no'?: number;
// (undocumented)
'apiCalls.savedObjectsImport.createNewCopiesEnabled.yes'?: number;
// (undocumented)
'apiCalls.savedObjectsImport.kibanaRequest.no'?: number;
// (undocumented)
'apiCalls.savedObjectsImport.kibanaRequest.yes'?: number;
// (undocumented)
'apiCalls.savedObjectsImport.overwriteEnabled.no'?: number;
// (undocumented)
'apiCalls.savedObjectsImport.overwriteEnabled.yes'?: number;
// (undocumented)
'apiCalls.savedObjectsImport.total'?: number;
// (undocumented)
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.no'?: number;
// (undocumented)
'apiCalls.savedObjectsResolveImportErrors.createNewCopiesEnabled.yes'?: number;
// (undocumented)
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.no'?: number;
// (undocumented)
'apiCalls.savedObjectsResolveImportErrors.kibanaRequest.yes'?: number;
// (undocumented)
'apiCalls.savedObjectsResolveImportErrors.total'?: number;
}
// @public (undocumented)
export interface CountResponse {
// (undocumented)
@ -2367,6 +2405,12 @@ export interface SavedObjectsImportUnsupportedTypeError {
type: 'unsupported_type';
}
// @public (undocumented)
export interface SavedObjectsIncrementCounterField {
fieldName: string;
incrementBy?: number;
}
// @public (undocumented)
export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOptions {
initialize?: boolean;
@ -2448,7 +2492,7 @@ export class SavedObjectsRepository {
// (undocumented)
find<T = unknown>(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse<T>>;
get<T = unknown>(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<SavedObject<T>>;
incrementCounter(type: string, id: string, counterFieldNames: string[], options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject>;
incrementCounter<T = unknown>(type: string, id: string, counterFields: Array<string | SavedObjectsIncrementCounterField>, options?: SavedObjectsIncrementCounterOptions): Promise<SavedObject<T>>;
removeReferencesTo(type: string, id: string, options?: SavedObjectsRemoveReferencesToOptions): Promise<SavedObjectsRemoveReferencesToResponse>;
update<T = unknown>(type: string, id: string, attributes: Partial<T>, options?: SavedObjectsUpdateOptions): Promise<SavedObjectsUpdateResponse<T>>;
}
@ -2753,7 +2797,7 @@ export interface UiSettingsParams<T = unknown> {
description?: string;
// @deprecated
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
name?: string;

View file

@ -185,6 +185,7 @@ test(`doesn't setup core services if config validation fails`, async () => {
expect(mockElasticsearchService.setup).not.toHaveBeenCalled();
expect(mockPluginsService.setup).not.toHaveBeenCalled();
expect(mockLegacyService.setup).not.toHaveBeenCalled();
expect(mockSavedObjectsService.stop).not.toHaveBeenCalled();
expect(mockUiSettingsService.setup).not.toHaveBeenCalled();
expect(mockRenderingService.setup).not.toHaveBeenCalled();
expect(mockMetricsService.setup).not.toHaveBeenCalled();

View file

@ -31,7 +31,7 @@ import { LegacyService, ensureValidConfiguration } from './legacy';
import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging';
import { UiSettingsService } from './ui_settings';
import { PluginsService, config as pluginsConfig } from './plugins';
import { SavedObjectsService } from './saved_objects';
import { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects';
import { MetricsService, opsConfig } from './metrics';
import { CapabilitiesService } from './capabilities';
import { EnvironmentService, config as pidConfig } from './environment';
@ -78,6 +78,9 @@ export class Server {
private readonly coreUsageData: CoreUsageDataService;
private readonly i18n: I18nService;
private readonly savedObjectsStartPromise: Promise<SavedObjectsServiceStart>;
private resolveSavedObjectsStartPromise?: (value: SavedObjectsServiceStart) => void;
#pluginsInitialized?: boolean;
private coreStart?: InternalCoreStart;
private readonly logger: LoggerFactory;
@ -109,6 +112,10 @@ export class Server {
this.logging = new LoggingService(core);
this.coreUsageData = new CoreUsageDataService(core);
this.i18n = new I18nService(core);
this.savedObjectsStartPromise = new Promise((resolve) => {
this.resolveSavedObjectsStartPromise = resolve;
});
}
public async setup() {
@ -155,9 +162,17 @@ export class Server {
http: httpSetup,
});
const metricsSetup = await this.metrics.setup({ http: httpSetup });
const coreUsageDataSetup = this.coreUsageData.setup({
metrics: metricsSetup,
savedObjectsStartPromise: this.savedObjectsStartPromise,
});
const savedObjectsSetup = await this.savedObjects.setup({
http: httpSetup,
elasticsearch: elasticsearchServiceSetup,
coreUsageData: coreUsageDataSetup,
});
const uiSettingsSetup = await this.uiSettings.setup({
@ -165,8 +180,6 @@ export class Server {
savedObjects: savedObjectsSetup,
});
const metricsSetup = await this.metrics.setup({ http: httpSetup });
const statusSetup = await this.status.setup({
elasticsearch: elasticsearchServiceSetup,
pluginDependencies: pluginTree.asNames,
@ -191,8 +204,6 @@ export class Server {
loggingSystem: this.loggingSystem,
});
this.coreUsageData.setup({ metrics: metricsSetup });
const coreSetup: InternalCoreSetup = {
capabilities: capabilitiesSetup,
context: contextServiceSetup,
@ -235,6 +246,8 @@ export class Server {
elasticsearch: elasticsearchStart,
pluginsInitialized: this.#pluginsInitialized,
});
await this.resolveSavedObjectsStartPromise!(savedObjectsStart);
soStartSpan?.end();
const capabilitiesStart = this.capabilities.start();
const uiSettingsStart = await this.uiSettings.start();

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Type } from '@kbn/config-schema';
import { UiStatsMetricType } from '@kbn/analytics';
import { UiCounterMetricType } from '@kbn/analytics';
/**
* UI element type to represent the settings.
@ -87,7 +87,7 @@ export interface UiSettingsParams<T = unknown> {
* Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place
*/
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
}

View file

@ -25,12 +25,19 @@ export const CreateDebPackage: Task = {
description: 'Creating deb package',
async run(config, log, build) {
await runFpm(config, log, build, 'deb', [
await runFpm(config, log, build, 'deb', 'x64', [
'--architecture',
'amd64',
'--deb-priority',
'optional',
]);
await runFpm(config, log, build, 'deb', 'arm64', [
'--architecture',
'arm64',
'--deb-priority',
'optional',
]);
},
};
@ -38,7 +45,18 @@ export const CreateRpmPackage: Task = {
description: 'Creating rpm package',
async run(config, log, build) {
await runFpm(config, log, build, 'rpm', ['--architecture', 'x86_64', '--rpm-os', 'linux']);
await runFpm(config, log, build, 'rpm', 'x64', [
'--architecture',
'x86_64',
'--rpm-os',
'linux',
]);
await runFpm(config, log, build, 'rpm', 'arm64', [
'--architecture',
'aarch64',
'--rpm-os',
'linux',
]);
},
};

View file

@ -28,9 +28,10 @@ export async function runFpm(
log: ToolingLog,
build: Build,
type: 'rpm' | 'deb',
architecture: 'arm64' | 'x64',
pkgSpecificFlags: string[]
) {
const linux = config.getPlatform('linux', 'x64');
const linux = config.getPlatform('linux', architecture);
const version = config.getBuildVersion();
const resolveWithTrailingSlash = (...paths: string[]) => `${resolve(...paths)}/`;

View file

@ -27,13 +27,13 @@ import { File } from '../file';
* Get the files that are staged for commit (excluding deleted files)
* as `File` objects that are aware of their commit status.
*
* @param {String} repoPath
* @param {String} gitRef
* @return {Promise<Array<File>>}
*/
export async function getFilesForCommit() {
export async function getFilesForCommit(gitRef) {
const simpleGit = new SimpleGit(REPO_ROOT);
const output = await fcb((cb) => simpleGit.diff(['--name-status', '--cached'], cb));
const gitRefForDiff = gitRef ? gitRef : '--cached';
const output = await fcb((cb) => simpleGit.diff(['--name-status', gitRefForDiff], cb));
return (
output

View file

@ -17,16 +17,30 @@
* under the License.
*/
import { run, combineErrors } from '@kbn/dev-utils';
import { run, combineErrors, createFlagError } from '@kbn/dev-utils';
import * as Eslint from './eslint';
import * as Sasslint from './sasslint';
import { getFilesForCommit, checkFileCasing } from './precommit_hook';
run(
async ({ log, flags }) => {
const files = await getFilesForCommit();
const files = await getFilesForCommit(flags.ref);
const errors = [];
const maxFilesCount = flags['max-files']
? Number.parseInt(String(flags['max-files']), 10)
: undefined;
if (maxFilesCount !== undefined && (!Number.isFinite(maxFilesCount) || maxFilesCount < 1)) {
throw createFlagError('expected --max-files to be a number greater than 0');
}
if (maxFilesCount && files.length > maxFilesCount) {
log.warning(
`--max-files is set to ${maxFilesCount} and ${files.length} were discovered. The current script execution will be skipped.`
);
return;
}
try {
await checkFileCasing(log, files);
} catch (error) {
@ -52,15 +66,18 @@ run(
},
{
description: `
Run checks on files that are staged for commit
Run checks on files that are staged for commit by default
`,
flags: {
boolean: ['fix'],
string: ['max-files', 'ref'],
default: {
fix: false,
},
help: `
--fix Execute eslint in --fix mode
--max-files Max files number to check against. If exceeded the script will skip the execution
--ref Run checks against any git ref files (example HEAD or <commit_sha>) instead of running against staged ones
`,
},
}

View file

@ -22,7 +22,7 @@ import { Subscription } from 'rxjs';
import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui';
import { useParams } from 'react-router-dom';
import { UiStatsMetricType } from '@kbn/analytics';
import { UiCounterMetricType } from '@kbn/analytics';
import { CallOuts } from './components/call_outs';
import { Search } from './components/search';
import { Form } from './components/form';
@ -40,7 +40,7 @@ interface AdvancedSettingsProps {
dockLinks: DocLinksStart['links'];
toasts: ToastsStart;
componentRegistry: ComponentRegistry['start'];
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}
interface AdvancedSettingsComponentProps extends AdvancedSettingsProps {

View file

@ -36,7 +36,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { isEmpty } from 'lodash';
import { i18n } from '@kbn/i18n';
import { UiStatsMetricType } from '@kbn/analytics';
import { UiCounterMetricType } from '@kbn/analytics';
import { toMountPoint } from '../../../../../kibana_react/public';
import { DocLinksStart, ToastsStart } from '../../../../../../core/public';
@ -57,7 +57,7 @@ interface FormProps {
enableSaving: boolean;
dockLinks: DocLinksStart['links'];
toasts: ToastsStart;
trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void;
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}
interface FormState {

View file

@ -57,7 +57,7 @@ export async function mountManagementSection(
const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices();
const canSave = application.capabilities.advancedSettings.save as boolean;
const trackUiMetric = usageCollection?.reportUiStats.bind(usageCollection, 'advanced_settings');
const trackUiMetric = usageCollection?.reportUiCounter.bind(usageCollection, 'advanced_settings');
if (!canSave) {
chrome.setBadge(readOnlyBadge);

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { UiStatsMetricType } from '@kbn/analytics';
import { UiCounterMetricType } from '@kbn/analytics';
import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public';
export interface FieldSetting {
@ -41,7 +41,7 @@ export interface FieldSetting {
docLinksKey: string;
};
metric?: {
type: UiStatsMetricType;
type: UiCounterMetricType;
name: string;
};
}

View file

@ -17,15 +17,15 @@
* under the License.
*/
import { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import { MetricsTracker } from '../types';
import { UsageCollectionSetup } from '../../../usage_collection/public';
const APP_TRACKER_NAME = 'console';
export const createUsageTracker = (usageCollection?: UsageCollectionSetup): MetricsTracker => {
const track = (type: UiStatsMetricType, name: string) =>
usageCollection?.reportUiStats(APP_TRACKER_NAME, type, name);
const track = (type: UiCounterMetricType, name: string) =>
usageCollection?.reportUiCounter(APP_TRACKER_NAME, type, name);
return {
count: (eventName: string) => {

View file

@ -304,7 +304,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = `
url="/plugins/home/assets/welcome_graphic_light_2x.png"
>
<figure
className="euiImage "
className="euiImage euiImage--original"
>
<img
alt=""
@ -1023,7 +1023,7 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`]
url="/plugins/home/assets/welcome_graphic_light_2x.png"
>
<figure
className="euiImage "
className="euiImage euiImage--original"
>
<img
alt=""

View file

@ -50,7 +50,6 @@ export function LibraryNotificationPopover({
return (
<EuiPopover
ownFocus
button={
<EuiButtonIcon
data-test-subj={`embeddablePanelNotification-${id}`}

View file

@ -65,7 +65,11 @@ export function migrateAppState(
if (usageCollection) {
// This will help us figure out when to remove support for older style URLs.
usageCollection.reportUiStats('DashboardPanelVersionInUrl', METRIC_TYPE.LOADED, `${version}`);
usageCollection.reportUiCounter(
'DashboardPanelVersionInUrl',
METRIC_TYPE.LOADED,
`${version}`
);
}
return semverSatisfies(version, '<7.3');

View file

@ -27,11 +27,10 @@ import {
FieldFormatInstanceType,
FieldFormatId,
IFieldFormatMetaParams,
IFieldFormat,
} from './types';
import { baseFormatters } from './constants/base_formatters';
import { FieldFormat } from './field_format';
import { SerializedFieldFormat } from '../../../expressions/common/types';
import { FormatFactory } from './utils';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../kbn_field_types/types';
import { UI_SETTINGS } from '../constants';
import { FieldFormatNotFoundError } from '../field_formats';
@ -42,7 +41,7 @@ export class FieldFormatsRegistry {
protected metaParamsOptions: Record<string, any> = {};
protected getConfig?: FieldFormatsGetConfigFn;
// overriden on the public contract
public deserialize: (mapping: SerializedFieldFormat) => IFieldFormat = () => {
public deserialize: FormatFactory = () => {
return new (FieldFormat.from(identity))();
};

View file

@ -17,9 +17,4 @@
* under the License.
*/
export interface Stats {
min: number;
max: number;
sum: number;
avg: number;
}
export * from './load_index_pattern';

View file

@ -0,0 +1,65 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { IndexPatternsContract } from '../index_patterns';
import { IndexPatternSpec } from '..';
const name = 'indexPatternLoad';
type Input = null;
type Output = Promise<{ type: 'index_pattern'; value: IndexPatternSpec }>;
interface Arguments {
id: string;
}
/** @internal */
export interface IndexPatternLoadStartDependencies {
indexPatterns: IndexPatternsContract;
}
export type IndexPatternLoadExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof name,
Input,
Arguments,
Output
>;
export const getIndexPatternLoadMeta = (): Omit<
IndexPatternLoadExpressionFunctionDefinition,
'fn'
> => ({
name,
type: 'index_pattern',
inputTypes: ['null'],
help: i18n.translate('data.functions.indexPatternLoad.help', {
defaultMessage: 'Loads an index pattern',
}),
args: {
id: {
types: ['string'],
required: true,
help: i18n.translate('data.functions.indexPatternLoad.id.help', {
defaultMessage: 'index pattern id to load',
}),
},
},
});

View file

@ -33,6 +33,7 @@ describe('AggType Class', () => {
test('assigns the config value to itself', () => {
const config: AggTypeConfig = {
name: 'name',
expressionName: 'aggName',
title: 'title',
};
@ -48,6 +49,7 @@ describe('AggType Class', () => {
const aggConfig = {} as IAggConfig;
const config: AggTypeConfig = {
name: 'name',
expressionName: 'aggName',
title: 'title',
makeLabel,
};
@ -65,6 +67,7 @@ describe('AggType Class', () => {
const aggType = new AggType({
name: 'name',
expressionName: 'aggName',
title: 'title',
getResponseAggs: testConfig,
getRequestAggs: testConfig,
@ -78,6 +81,7 @@ describe('AggType Class', () => {
const aggConfig = {} as IAggConfig;
const aggType = new AggType({
name: 'name',
expressionName: 'aggName',
title: 'title',
});
const responseAggs = aggType.getRequestAggs(aggConfig);
@ -90,6 +94,7 @@ describe('AggType Class', () => {
test('defaults to AggParams object with JSON param', () => {
const aggType = new AggType({
name: 'smart agg',
expressionName: 'aggSmart',
title: 'title',
});
@ -102,6 +107,7 @@ describe('AggType Class', () => {
test('disables json param', () => {
const aggType = new AggType({
name: 'name',
expressionName: 'aggName',
title: 'title',
json: false,
});
@ -113,6 +119,7 @@ describe('AggType Class', () => {
test('can disable customLabel', () => {
const aggType = new AggType({
name: 'smart agg',
expressionName: 'aggSmart',
title: 'title',
customLabels: false,
});
@ -127,6 +134,7 @@ describe('AggType Class', () => {
const aggType = new AggType({
name: 'bucketeer',
expressionName: 'aggBucketeer',
title: 'title',
params,
});
@ -153,6 +161,7 @@ describe('AggType Class', () => {
} as unknown) as IAggConfig;
const aggType = new AggType({
name: 'name',
expressionName: 'aggName',
title: 'title',
});
expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(`
@ -168,6 +177,7 @@ describe('AggType Class', () => {
} as unknown) as IAggConfig;
const aggType = new AggType({
name: 'name',
expressionName: 'aggName',
title: 'title',
});
expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(`Object {}`);
@ -186,6 +196,7 @@ describe('AggType Class', () => {
const getSerializedFormat = jest.fn().mockReturnValue({ id: 'hello' });
const aggType = new AggType({
name: 'name',
expressionName: 'aggName',
title: 'title',
getSerializedFormat,
});

View file

@ -39,7 +39,7 @@ export interface AggTypeConfig<
createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any;
type?: string;
dslName?: string;
expressionName?: string;
expressionName: string;
makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string);
ordered?: any;
hasNoDsl?: boolean;
@ -90,12 +90,11 @@ export class AggType<
dslName: string;
/**
* the name of the expression function that this aggType represents.
* TODO: this should probably be a required field.
*
* @property name
* @type {string}
*/
expressionName?: string;
expressionName: string;
/**
* the user friendly name that will be shown in the ui for this aggType
*

View file

@ -27,6 +27,7 @@ import { intervalOptions, autoInterval, isAutoInterval } from './_interval_optio
import { createFilterDateHistogram } from './create_filter/date_histogram';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { aggDateHistogramFnName } from './date_histogram_fn';
import { ExtendedBounds } from './lib/extended_bounds';
import { TimeBuckets } from './lib/time_buckets';
@ -87,6 +88,7 @@ export const getDateHistogramBucketAgg = ({
}: DateHistogramBucketAggDependencies) =>
new BucketAggType<IBucketDateHistogramAggConfig>({
name: BUCKET_TYPES.DATE_HISTOGRAM,
expressionName: aggDateHistogramFnName,
title: i18n.translate('data.search.aggs.buckets.dateHistogramTitle', {
defaultMessage: 'Date Histogram',
}),

View file

@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggDateHistogram';
export const aggDateHistogramFnName = 'aggDateHistogram';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_HISTOGRAM>;
@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_HISTOGRAM>;
type Arguments = Assign<AggArgs, { timeRange?: string; extended_bounds?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggDateHistogramFnName,
Input,
Arguments,
Output
>;
export const aggDateHistogram = (): FunctionDefinition => ({
name: fnName,
name: aggDateHistogramFnName,
help: i18n.translate('data.search.aggs.function.buckets.dateHistogram.help', {
defaultMessage: 'Generates a serialized agg config for a Histogram agg',
}),

View file

@ -74,6 +74,31 @@ describe('date_range params', () => {
);
};
test('produces the expected expression ast', () => {
const aggConfigs = getAggConfigs();
const dateRange = aggConfigs.aggs[0];
expect(dateRange.toExpressionAst()).toMatchInlineSnapshot(`
Object {
"arguments": Object {
"enabled": Array [
true,
],
"id": Array [
"date_range",
],
"ranges": Array [
"[{\\"from\\":\\"now-1w/w\\",\\"to\\":\\"now\\"}]",
],
"schema": Array [
"buckets",
],
},
"function": "aggDateRange",
"type": "function",
}
`);
});
describe('getKey', () => {
test('should return object', () => {
const aggConfigs = getAggConfigs();

View file

@ -24,6 +24,7 @@ import { i18n } from '@kbn/i18n';
import { BUCKET_TYPES } from './bucket_agg_types';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { createFilterDateRange } from './create_filter/date_range';
import { aggDateRangeFnName } from './date_range_fn';
import { DateRangeKey } from './lib/date_range';
import { KBN_FIELD_TYPES } from '../../../../common/kbn_field_types/types';
@ -50,6 +51,7 @@ export const getDateRangeBucketAgg = ({
}: DateRangeBucketAggDependencies) =>
new BucketAggType({
name: BUCKET_TYPES.DATE_RANGE,
expressionName: aggDateRangeFnName,
title: dateRangeTitle,
createFilter: createFilterDateRange,
getKey({ from, to }): DateRangeKey {

View file

@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggDateRange';
export const aggDateRangeFnName = 'aggDateRange';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_RANGE>;
@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.DATE_RANGE>;
type Arguments = Assign<AggArgs, { ranges?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggDateRangeFnName,
Input,
Arguments,
Output
>;
export const aggDateRange = (): FunctionDefinition => ({
name: fnName,
name: aggDateRangeFnName,
help: i18n.translate('data.search.aggs.function.buckets.dateRange.help', {
defaultMessage: 'Generates a serialized agg config for a Date Range agg',
}),

View file

@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GeoBoundingBox } from './lib/geo_point';
import { aggFilterFnName } from './filter_fn';
import { BaseAggParams } from '../types';
const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', {
@ -34,6 +35,7 @@ export interface AggParamsFilter extends BaseAggParams {
export const getFilterBucketAgg = () =>
new BucketAggType({
name: BUCKET_TYPES.FILTER,
expressionName: aggFilterFnName,
title: filterTitle,
makeLabel: () => filterTitle,
params: [

View file

@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggFilter';
export const aggFilterFnName = 'aggFilter';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTER>;
@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTER>;
type Arguments = Assign<AggArgs, { geo_bounding_box?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggFilterFnName,
Input,
Arguments,
Output
>;
export const aggFilter = (): FunctionDefinition => ({
name: fnName,
name: aggFilterFnName,
help: i18n.translate('data.search.aggs.function.buckets.filter.help', {
defaultMessage: 'Generates a serialized agg config for a Filter agg',
}),

View file

@ -74,6 +74,33 @@ describe('Filters Agg', () => {
},
});
test('produces the expected expression ast', () => {
const aggConfigs = getAggConfigs({
filters: [
generateFilter('a', 'lucene', 'foo'),
generateFilter('b', 'lucene', 'status:200'),
generateFilter('c', 'lucene', 'status:[400 TO 499] AND (foo OR bar)'),
],
});
expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(`
Object {
"arguments": Object {
"enabled": Array [
true,
],
"filters": Array [
"[{\\"label\\":\\"a\\",\\"input\\":{\\"language\\":\\"lucene\\",\\"query\\":\\"foo\\"}},{\\"label\\":\\"b\\",\\"input\\":{\\"language\\":\\"lucene\\",\\"query\\":\\"status:200\\"}},{\\"label\\":\\"c\\",\\"input\\":{\\"language\\":\\"lucene\\",\\"query\\":\\"status:[400 TO 499] AND (foo OR bar)\\"}}]",
],
"id": Array [
"test",
],
},
"function": "aggFilters",
"type": "function",
}
`);
});
describe('using Lucene', () => {
test('works with lucene filters', () => {
const aggConfigs = getAggConfigs({

View file

@ -24,6 +24,7 @@ import { createFilterFilters } from './create_filter/filters';
import { toAngularJSON } from '../utils';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { aggFiltersFnName } from './filters_fn';
import { getEsQueryConfig, buildEsQuery, Query, UI_SETTINGS } from '../../../../common';
import { BaseAggParams } from '../types';
@ -53,6 +54,7 @@ export interface AggParamsFilters extends Omit<BaseAggParams, 'customLabel'> {
export const getFiltersBucketAgg = ({ getConfig }: FiltersBucketAggDependencies) =>
new BucketAggType({
name: BUCKET_TYPES.FILTERS,
expressionName: aggFiltersFnName,
title: filtersTitle,
createFilter: createFilterFilters,
customLabels: false,

View file

@ -23,7 +23,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggFilters';
export const aggFiltersFnName = 'aggFilters';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTERS>;
@ -31,10 +31,15 @@ type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTERS>;
type Arguments = Assign<AggArgs, { filters?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggFiltersFnName,
Input,
Arguments,
Output
>;
export const aggFilters = (): FunctionDefinition => ({
name: fnName,
name: aggFiltersFnName,
help: i18n.translate('data.search.aggs.function.buckets.filters.help', {
defaultMessage: 'Generates a serialized agg config for a Filter agg',
}),

View file

@ -87,6 +87,42 @@ describe('Geohash Agg', () => {
});
});
test('produces the expected expression ast', () => {
const aggConfigs = getAggConfigs();
expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(`
Object {
"arguments": Object {
"autoPrecision": Array [
true,
],
"enabled": Array [
true,
],
"field": Array [
"location",
],
"id": Array [
"geohash_grid",
],
"isFilteredByCollar": Array [
true,
],
"precision": Array [
2,
],
"schema": Array [
"segment",
],
"useGeocentroid": Array [
true,
],
},
"function": "aggGeoHash",
"type": "function",
}
`);
});
describe('getRequestAggs', () => {
describe('initial aggregation creation', () => {
let aggConfigs: IAggConfigs;

View file

@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { KBN_FIELD_TYPES } from '../../../../common';
import { BUCKET_TYPES } from './bucket_agg_types';
import { aggGeoHashFnName } from './geo_hash_fn';
import { GeoBoundingBox } from './lib/geo_point';
import { BaseAggParams } from '../types';
@ -47,6 +48,7 @@ export interface AggParamsGeoHash extends BaseAggParams {
export const getGeoHashBucketAgg = () =>
new BucketAggType<IBucketAggConfig>({
name: BUCKET_TYPES.GEOHASH_GRID,
expressionName: aggGeoHashFnName,
title: geohashGridTitle,
makeLabel: () => geohashGridTitle,
params: [

View file

@ -23,17 +23,22 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggGeoHash';
export const aggGeoHashFnName = 'aggGeoHash';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.GEOHASH_GRID>;
type Arguments = Assign<AggArgs, { boundingBox?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, Arguments, Output>;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggGeoHashFnName,
Input,
Arguments,
Output
>;
export const aggGeoHash = (): FunctionDefinition => ({
name: fnName,
name: aggGeoHashFnName,
help: i18n.translate('data.search.aggs.function.buckets.geoHash.help', {
defaultMessage: 'Generates a serialized agg config for a Geo Hash agg',
}),

View file

@ -22,6 +22,7 @@ import { noop } from 'lodash';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { aggGeoTileFnName } from './geo_tile_fn';
import { KBN_FIELD_TYPES } from '../../../../common';
import { METRIC_TYPES } from '../metrics/metric_agg_types';
import { BaseAggParams } from '../types';
@ -39,6 +40,7 @@ export interface AggParamsGeoTile extends BaseAggParams {
export const getGeoTitleBucketAgg = () =>
new BucketAggType({
name: BUCKET_TYPES.GEOTILE_GRID,
expressionName: aggGeoTileFnName,
title: geotileGridTitle,
params: [
{

View file

@ -22,16 +22,21 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
import { getParsedValue } from '../utils/get_parsed_value';
const fnName = 'aggGeoTile';
export const aggGeoTileFnName = 'aggGeoTile';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.GEOTILE_GRID>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<typeof fnName, Input, AggArgs, Output>;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggGeoTileFnName,
Input,
AggArgs,
Output
>;
export const aggGeoTile = (): FunctionDefinition => ({
name: fnName,
name: aggGeoTileFnName,
help: i18n.translate('data.search.aggs.function.buckets.geoTile.help', {
defaultMessage: 'Generates a serialized agg config for a Geo Tile agg',
}),

View file

@ -72,6 +72,50 @@ describe('Histogram Agg', () => {
return aggConfigs.aggs[0].toDsl()[BUCKET_TYPES.HISTOGRAM];
};
test('produces the expected expression ast', () => {
const aggConfigs = getAggConfigs({
intervalBase: 100,
field: {
name: 'field',
},
});
expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(`
Object {
"arguments": Object {
"enabled": Array [
true,
],
"extended_bounds": Array [
"{\\"min\\":\\"\\",\\"max\\":\\"\\"}",
],
"field": Array [
"field",
],
"has_extended_bounds": Array [
false,
],
"id": Array [
"test",
],
"interval": Array [
"auto",
],
"intervalBase": Array [
100,
],
"min_doc_count": Array [
false,
],
"schema": Array [
"segment",
],
},
"function": "aggHistogram",
"type": "function",
}
`);
});
describe('ordered', () => {
let histogramType: BucketAggType<IBucketHistogramAggConfig>;

View file

@ -27,6 +27,7 @@ import { BaseAggParams } from '../types';
import { BucketAggType, IBucketAggConfig } from './bucket_agg_type';
import { createFilterHistogram } from './create_filter/histogram';
import { BUCKET_TYPES } from './bucket_agg_types';
import { aggHistogramFnName } from './histogram_fn';
import { ExtendedBounds } from './lib/extended_bounds';
import { isAutoInterval, autoInterval } from './_interval_options';
import { calculateHistogramInterval } from './lib/histogram_calculate_interval';
@ -62,6 +63,7 @@ export const getHistogramBucketAgg = ({
}: HistogramBucketAggDependencies) =>
new BucketAggType<IBucketHistogramAggConfig>({
name: BUCKET_TYPES.HISTOGRAM,
expressionName: aggHistogramFnName,
title: i18n.translate('data.search.aggs.buckets.histogramTitle', {
defaultMessage: 'Histogram',
}),

Some files were not shown because too many files have changed in this diff Show more