mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Merge branch 'master' into write-to-es-strawman
This commit is contained in:
commit
3a16056457
801 changed files with 18775 additions and 5910 deletions
|
@ -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
|
||||
|
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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<string, string></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 |
|
||||
|
|
|
@ -15,7 +15,7 @@ Metric to track once this property changes
|
|||
|
||||
```typescript
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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) | |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) > [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;
|
||||
```
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsIncrementCounterField](./kibana-plugin-core-server.savedobjectsincrementcounterfield.md) > [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;
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [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). |
|
||||
|
|
@ -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<string | SavedObjectsIncrementCounterField></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
|
||||
|
||||
|
|
|
@ -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 |
|
||||
|
||||
|
|
|
@ -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<string, string></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 |
|
||||
|
|
|
@ -15,7 +15,7 @@ Metric to track once this property changes
|
|||
|
||||
```typescript
|
||||
metric?: {
|
||||
type: UiStatsMetricType;
|
||||
type: UiCounterMetricType;
|
||||
name: string;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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) | \* |
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.getname.md)
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md)
|
||||
|
||||
## SearchSessionInfoProvider.getName property
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.geturlgeneratordata.md)
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) > [getUrlGeneratorData](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.geturlgeneratordata.md)
|
||||
|
||||
## SearchSessionInfoProvider.getUrlGeneratorData property
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchSessionInfoprovider.md)
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [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>() => Promise<string></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>() => Promise<{</code><br/><code> urlGeneratorId: ID;</code><br/><code> initialState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> restoreState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> }></code> | |
|
||||
| [getName](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.getname.md) | <code>() => Promise<string></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>() => Promise<{</code><br/><code> urlGeneratorId: ID;</code><br/><code> initialState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> restoreState: UrlGeneratorStateMapping[ID]['State'];</code><br/><code> }></code> | |
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
## SearchSourceFields.fields property
|
||||
|
||||
Retrieve fields via the search Fields API
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
fields?: NameList;
|
||||
fields?: SearchFieldValue[];
|
||||
```
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) > [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;
|
||||
```
|
|
@ -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[] | Filter | (() => Filter[] | Filter | 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> | |
|
||||
|
|
|
@ -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) | | |
|
||||
|
||||
|
|
|
@ -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<DataPluginStartDependencies, DataPluginStart></code> | |
|
||||
| { expressions } | <code>IndexPatternsServiceSetupDeps</code> | |
|
||||
|
||||
<b>Returns:</b>
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [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;
|
||||
```
|
|
@ -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>() => 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><T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>></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>() => ExecutionContextSearch</code> | Get search context of the expression. |
|
||||
| [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | <code>() => string | undefined</code> | Search context in which expression should operate. |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [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;
|
||||
```
|
|
@ -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>() => 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><T extends SavedObjectAttributes = SavedObjectAttributes>(type: string, id: string) => Promise<SavedObject<T>></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>() => ExecutionContextSearch</code> | Get search context of the expression. |
|
||||
| [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | <code>() => string | undefined</code> | Search context in which expression should operate. |
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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 || '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -319,7 +319,6 @@ class HeaderHelpMenuUI extends Component<Props, State> {
|
|||
data-test-subj="helpMenuButton"
|
||||
id="headerHelpMenu"
|
||||
isOpen={this.state.isOpen}
|
||||
ownFocus
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
|
|
|
@ -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;
|
||||
|
|
24
src/core/server/core_usage_data/constants.ts
Normal file
24
src/core/server/core_usage_data/constants.ts
Normal 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';
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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|');
|
||||
|
|
|
@ -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) {
|
||||
|
|
32
src/core/server/core_usage_data/core_usage_stats.ts
Normal file
32
src/core/server/core_usage_data/core_usage_stats.ts
Normal 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: {},
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
};
|
227
src/core/server/core_usage_data/core_usage_stats_client.test.ts
Normal file
227
src/core/server/core_usage_data/core_usage_stats_client.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
154
src/core/server/core_usage_data/core_usage_stats_client.ts
Normal file
154
src/core/server/core_usage_data/core_usage_stats_client.ts
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -48,6 +48,7 @@ export {
|
|||
export {
|
||||
ISavedObjectsRepository,
|
||||
SavedObjectsIncrementCounterOptions,
|
||||
SavedObjectsIncrementCounterField,
|
||||
SavedObjectsDeleteByNamespaceOptions,
|
||||
} from './service/lib/repository';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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/');
|
||||
|
||||
|
|
|
@ -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'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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)}/`;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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=""
|
||||
|
|
|
@ -50,7 +50,6 @@ export function LibraryNotificationPopover({
|
|||
|
||||
return (
|
||||
<EuiPopover
|
||||
ownFocus
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`embeddablePanelNotification-${id}`}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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))();
|
||||
};
|
||||
|
||||
|
|
|
@ -17,9 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export interface Stats {
|
||||
min: number;
|
||||
max: number;
|
||||
sum: number;
|
||||
avg: number;
|
||||
}
|
||||
export * from './load_index_pattern';
|
|
@ -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',
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue