diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f27885c1e32c3..d14556ea1dabf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -348,6 +348,7 @@ # Security Solution sub teams /x-pack/plugins/case @elastic/security-threat-hunting +/x-pack/plugins/timelines @elastic/security-threat-hunting /x-pack/test/case_api_integration @elastic/security-threat-hunting /x-pack/plugins/lists @elastic/security-detections-response diff --git a/docs/api/actions-and-connectors/legacy/create.asciidoc b/docs/api/actions-and-connectors/legacy/create.asciidoc index af4feddcb80fb..0361c4222986b 100644 --- a/docs/api/actions-and-connectors/legacy/create.asciidoc +++ b/docs/api/actions-and-connectors/legacy/create.asciidoc @@ -4,7 +4,7 @@ Legacy Create connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Creates a connector. diff --git a/docs/api/actions-and-connectors/legacy/delete.asciidoc b/docs/api/actions-and-connectors/legacy/delete.asciidoc index 170fceba2d157..9ec2c0d392a96 100644 --- a/docs/api/actions-and-connectors/legacy/delete.asciidoc +++ b/docs/api/actions-and-connectors/legacy/delete.asciidoc @@ -4,7 +4,7 @@ Legacy Delete connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Deletes a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/execute.asciidoc b/docs/api/actions-and-connectors/legacy/execute.asciidoc index 200844ab72f17..f01aa1585b192 100644 --- a/docs/api/actions-and-connectors/legacy/execute.asciidoc +++ b/docs/api/actions-and-connectors/legacy/execute.asciidoc @@ -4,7 +4,7 @@ Legacy Execute connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Executes a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/get.asciidoc b/docs/api/actions-and-connectors/legacy/get.asciidoc index 1b138fb7032e0..6413fce558f5b 100644 --- a/docs/api/actions-and-connectors/legacy/get.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get.asciidoc @@ -4,7 +4,7 @@ Legacy Get connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieves a connector by ID. diff --git a/docs/api/actions-and-connectors/legacy/get_all.asciidoc b/docs/api/actions-and-connectors/legacy/get_all.asciidoc index ba235955c005e..191eccb6f8d39 100644 --- a/docs/api/actions-and-connectors/legacy/get_all.asciidoc +++ b/docs/api/actions-and-connectors/legacy/get_all.asciidoc @@ -4,7 +4,7 @@ Legacy Get all connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieves all connectors. diff --git a/docs/api/actions-and-connectors/legacy/list.asciidoc b/docs/api/actions-and-connectors/legacy/list.asciidoc index 8acfd5415af57..d78838dcbe974 100644 --- a/docs/api/actions-and-connectors/legacy/list.asciidoc +++ b/docs/api/actions-and-connectors/legacy/list.asciidoc @@ -4,7 +4,7 @@ Legacy List all connector types ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieves a list of all connector types. diff --git a/docs/api/actions-and-connectors/legacy/update.asciidoc b/docs/api/actions-and-connectors/legacy/update.asciidoc index 517daf9a40dca..6a33e765cf063 100644 --- a/docs/api/actions-and-connectors/legacy/update.asciidoc +++ b/docs/api/actions-and-connectors/legacy/update.asciidoc @@ -4,7 +4,7 @@ Legacy Update connector ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Updates the attributes for an existing connector. diff --git a/docs/api/alerting/legacy/create.asciidoc b/docs/api/alerting/legacy/create.asciidoc index 5c594d64a3f45..8363569541356 100644 --- a/docs/api/alerting/legacy/create.asciidoc +++ b/docs/api/alerting/legacy/create.asciidoc @@ -4,7 +4,7 @@ Legacy create alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Create {kib} alerts. diff --git a/docs/api/alerting/legacy/delete.asciidoc b/docs/api/alerting/legacy/delete.asciidoc index 68851973cab5b..2af420f2bc34e 100644 --- a/docs/api/alerting/legacy/delete.asciidoc +++ b/docs/api/alerting/legacy/delete.asciidoc @@ -4,7 +4,7 @@ Legacy delete alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Permanently remove an alert. diff --git a/docs/api/alerting/legacy/disable.asciidoc b/docs/api/alerting/legacy/disable.asciidoc index 56e06371570c2..1a9b928bfba78 100644 --- a/docs/api/alerting/legacy/disable.asciidoc +++ b/docs/api/alerting/legacy/disable.asciidoc @@ -4,7 +4,7 @@ Legacy disable alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Disable an alert. diff --git a/docs/api/alerting/legacy/enable.asciidoc b/docs/api/alerting/legacy/enable.asciidoc index 913d96a84352b..da4b466d6fda4 100644 --- a/docs/api/alerting/legacy/enable.asciidoc +++ b/docs/api/alerting/legacy/enable.asciidoc @@ -4,7 +4,7 @@ Legacy enable alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Enable an alert. diff --git a/docs/api/alerting/legacy/find.asciidoc b/docs/api/alerting/legacy/find.asciidoc index 94d9bc425bd21..7c493e9c8eb5b 100644 --- a/docs/api/alerting/legacy/find.asciidoc +++ b/docs/api/alerting/legacy/find.asciidoc @@ -4,7 +4,7 @@ Legacy find alerts ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve a paginated set of alerts based on condition. diff --git a/docs/api/alerting/legacy/get.asciidoc b/docs/api/alerting/legacy/get.asciidoc index f1014d18e8774..ee0f52f51005a 100644 --- a/docs/api/alerting/legacy/get.asciidoc +++ b/docs/api/alerting/legacy/get.asciidoc @@ -4,7 +4,7 @@ Legacy get alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve an alert by ID. diff --git a/docs/api/alerting/legacy/health.asciidoc b/docs/api/alerting/legacy/health.asciidoc index b25307fb5efd1..68f04cc715bd7 100644 --- a/docs/api/alerting/legacy/health.asciidoc +++ b/docs/api/alerting/legacy/health.asciidoc @@ -4,7 +4,7 @@ Legacy get Alerting framework health ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve the health status of the Alerting framework. diff --git a/docs/api/alerting/legacy/list.asciidoc b/docs/api/alerting/legacy/list.asciidoc index e9ef3bbc27cd9..be37be36cd0e8 100644 --- a/docs/api/alerting/legacy/list.asciidoc +++ b/docs/api/alerting/legacy/list.asciidoc @@ -4,7 +4,7 @@ Legacy list all alert types ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Retrieve a list of all alert types. diff --git a/docs/api/alerting/legacy/mute.asciidoc b/docs/api/alerting/legacy/mute.asciidoc index dff42f5911e53..cf7adc446a2fd 100644 --- a/docs/api/alerting/legacy/mute.asciidoc +++ b/docs/api/alerting/legacy/mute.asciidoc @@ -4,7 +4,7 @@ Legacy mute alert instance ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Mute an alert instance. diff --git a/docs/api/alerting/legacy/mute_all.asciidoc b/docs/api/alerting/legacy/mute_all.asciidoc index df89fa15d1590..bc865480340e2 100644 --- a/docs/api/alerting/legacy/mute_all.asciidoc +++ b/docs/api/alerting/legacy/mute_all.asciidoc @@ -4,7 +4,7 @@ Legacy mute all alert instances ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Mute all alert instances. diff --git a/docs/api/alerting/legacy/unmute.asciidoc b/docs/api/alerting/legacy/unmute.asciidoc index 0be7e40dc1a19..300cf71b57a01 100644 --- a/docs/api/alerting/legacy/unmute.asciidoc +++ b/docs/api/alerting/legacy/unmute.asciidoc @@ -4,7 +4,7 @@ Legacy unmute alert instance ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Unmute an alert instance. diff --git a/docs/api/alerting/legacy/unmute_all.asciidoc b/docs/api/alerting/legacy/unmute_all.asciidoc index 8687c2d2fe8bb..3b0a7afe5f44d 100644 --- a/docs/api/alerting/legacy/unmute_all.asciidoc +++ b/docs/api/alerting/legacy/unmute_all.asciidoc @@ -4,7 +4,7 @@ Legacy unmute all alert instances ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Unmute all alert instances. diff --git a/docs/api/alerting/legacy/update.asciidoc b/docs/api/alerting/legacy/update.asciidoc index bffdf26c31400..b9cce995660e6 100644 --- a/docs/api/alerting/legacy/update.asciidoc +++ b/docs/api/alerting/legacy/update.asciidoc @@ -4,7 +4,7 @@ Legacy update alert ++++ -WARNING: Deprecated in 7.13.0. Use <> instead. +deprecated::[7.13.0,Use <> instead.] Update the attributes for an existing alert. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e1c2c40a31384..bcf74936077ec 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -537,6 +537,10 @@ Documentation: https://www.elastic.co/guide/en/kibana/master/task-manager-produc |Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. +|{kib-repo}blob/{branch}/x-pack/plugins/timelines/README.md[timelines] +|Timelines is a plugin that provides a grid component with accompanying server side apis to help users identify events of interest and perform root cause analysis within Kibana. + + |{kib-repo}blob/{branch}/x-pack/plugins/transform/readme.md[transform] |This plugin provides access to the transforms features provided by Elastic. diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md index a1fde4dec25b1..6e7b753320270 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md @@ -7,9 +7,9 @@ Signature: ```typescript -fieldIsTimeField(): boolean | "" | undefined; +fieldIsTimeField(): boolean; ``` Returns: -`boolean | "" | undefined` +`boolean` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md index c0ba1bbeea334..22f8994747aa2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md @@ -23,6 +23,7 @@ export declare class AggConfigs | [aggs](./kibana-plugin-plugins-data-public.aggconfigs.aggs.md) | | IAggConfig[] | | | [createAggConfig](./kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md) | | <T extends AggConfig = AggConfig>(params: CreateAggConfigParams, { addToAggConfigs }?: {
addToAggConfigs?: boolean | undefined;
}) => T | | | [indexPattern](./kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md) | | IndexPattern | | +| [timeFields](./kibana-plugin-plugins-data-public.aggconfigs.timefields.md) | | string[] | | | [timeRange](./kibana-plugin-plugins-data-public.aggconfigs.timerange.md) | | TimeRange | | ## Methods @@ -43,6 +44,7 @@ export declare class AggConfigs | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} | | [jsonDataEquals(aggConfigs)](./kibana-plugin-plugins-data-public.aggconfigs.jsondataequals.md) | | Data-by-data comparison of this Aggregation Ignores the non-array indexes | | [onSearchRequestStart(searchSource, options)](./kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md) | | | +| [setTimeFields(timeFields)](./kibana-plugin-plugins-data-public.aggconfigs.settimefields.md) | | | | [setTimeRange(timeRange)](./kibana-plugin-plugins-data-public.aggconfigs.settimerange.md) | | | | [toDsl(hierarchical)](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimefields.md new file mode 100644 index 0000000000000..31eadc5756d3d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimefields.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [setTimeFields](./kibana-plugin-plugins-data-public.aggconfigs.settimefields.md) + +## AggConfigs.setTimeFields() method + +Signature: + +```typescript +setTimeFields(timeFields: string[] | undefined): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| timeFields | string[] | undefined | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timefields.md new file mode 100644 index 0000000000000..903370fd8eb84 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timefields.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [timeFields](./kibana-plugin-plugins-data-public.aggconfigs.timefields.md) + +## AggConfigs.timeFields property + +Signature: + +```typescript +timeFields?: string[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md new file mode 100644 index 0000000000000..4e432b8d365a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggFunctionsMapping](./kibana-plugin-plugins-data-public.aggfunctionsmapping.md) > [aggSinglePercentile](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md) + +## AggFunctionsMapping.aggSinglePercentile property + +Signature: + +```typescript +aggSinglePercentile: ReturnType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md index 05388e2b86d7b..852c6d5f1c00b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.md @@ -45,6 +45,7 @@ export interface AggFunctionsMapping | [aggRange](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggrange.md) | ReturnType<typeof aggRange> | | | [aggSerialDiff](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggserialdiff.md) | ReturnType<typeof aggSerialDiff> | | | [aggSignificantTerms](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsignificantterms.md) | ReturnType<typeof aggSignificantTerms> | | +| [aggSinglePercentile](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md) | ReturnType<typeof aggSinglePercentile> | | | [aggStdDeviation](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggstddeviation.md) | ReturnType<typeof aggStdDeviation> | | | [aggSum](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsum.md) | ReturnType<typeof aggSum> | | | [aggTerms](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggterms.md) | ReturnType<typeof aggTerms> | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md index 3b5cecf1a0b82..bdae3ec738ac3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.metric_types.md @@ -32,6 +32,7 @@ export declare enum METRIC_TYPES | PERCENTILE\_RANKS | "percentile_ranks" | | | PERCENTILES | "percentiles" | | | SERIAL\_DIFF | "serial_diff" | | +| SINGLE\_PERCENTILE | "single_percentile" | | | STD\_DEV | "std_dev" | | | SUM | "sum" | | | SUM\_BUCKET | "sum_bucket" | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 440fd25993d64..cfaad01c029ea 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -14,7 +14,7 @@ search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; @@ -47,6 +47,11 @@ search: { intervalLabel: string; })[]; getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; + getDateHistogramMetaDataByDatatableColumn: (column: import("../../expressions").DatatableColumn) => { + interval: string | undefined; + timeZone: string | undefined; + timeRange: import("../common").TimeRange | undefined; + } | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md new file mode 100644 index 0000000000000..d1418d7245d73 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [AggFunctionsMapping](./kibana-plugin-plugins-data-server.aggfunctionsmapping.md) > [aggSinglePercentile](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md) + +## AggFunctionsMapping.aggSinglePercentile property + +Signature: + +```typescript +aggSinglePercentile: ReturnType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md index 86bf797572b09..6b5f854c155f3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.md @@ -45,6 +45,7 @@ export interface AggFunctionsMapping | [aggRange](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggrange.md) | ReturnType<typeof aggRange> | | | [aggSerialDiff](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggserialdiff.md) | ReturnType<typeof aggSerialDiff> | | | [aggSignificantTerms](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsignificantterms.md) | ReturnType<typeof aggSignificantTerms> | | +| [aggSinglePercentile](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md) | ReturnType<typeof aggSinglePercentile> | | | [aggStdDeviation](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggstddeviation.md) | ReturnType<typeof aggStdDeviation> | | | [aggSum](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsum.md) | ReturnType<typeof aggSum> | | | [aggTerms](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggterms.md) | ReturnType<typeof aggTerms> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md index d408f00e33c9e..b5c7d8931ad4b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.md @@ -14,6 +14,6 @@ export declare class IndexPatternsServiceProvider implements PluginSignature: ```typescript -setup(core: CoreSetup, { expressions }: IndexPatternsServiceSetupDeps): void; +setup(core: CoreSetup, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup<DataPluginStartDependencies, DataPluginStart> | | -| { expressions } | IndexPatternsServiceSetupDeps | | +| core | CoreSetup<IndexPatternsServiceStartDeps, DataPluginStart> | | +| { expressions, usageCollection } | IndexPatternsServiceSetupDeps | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md index 98f9310c6d98c..88079bb2fa3cb 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsserviceprovider.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }; ``` @@ -22,6 +22,6 @@ start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): Returns: `{ - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract, elasticsearchClient: ElasticsearchClient) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient) => Promise; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md index 250173d11a056..37f53af8971b3 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.metric_types.md @@ -32,6 +32,7 @@ export declare enum METRIC_TYPES | PERCENTILE\_RANKS | "percentile_ranks" | | | PERCENTILES | "percentiles" | | | SERIAL\_DIFF | "serial_diff" | | +| SINGLE\_PERCENTILE | "single_percentile" | | | STD\_DEV | "std_dev" | | | SUM | "sum" | | | SUM\_BUCKET | "sum_bucket" | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md index 4f8a0beefa421..0911c3e86964d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.search.md @@ -14,7 +14,7 @@ search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 446b6a2cfd851..a9de1888465f7 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -209,6 +209,32 @@ from *{stack-monitor-app}*. Turns off all unnecessary animations in the {kib} UI. Refresh the page to apply the changes. +[float] +[[kibana-banners-settings]] +==== Banners + +[NOTE] +==== +Banners are a https://www.elastic.co/subscriptions[subscription feature]. +==== + +[horizontal] +[[banners-placement]]`banners:placement`:: +Set to `Top` to display a banner above the Elastic header for this space. Defaults to the value of +the `xpack.banners.placement` configuration property. + +[[banners-textcontent]]`banners:textContent`:: +The text to display inside the banner for this space, either plain text or Markdown. +Defaults to the value of the `xpack.banners.textContent` configuration property. + +[[banners-textcolor]]`banners:textColor`:: +The color for the banner text for this space. Defaults to the value of +the `xpack.banners.textColor` configuration property. + +[[banners-backgroundcolor]]`banners:backgroundColor`:: +The color of the banner background for this space. Defaults to the value of +the `xpack.banners.backgroundColor` configuration property. + [float] [[kibana-dashboard-settings]] ==== Dashboard diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index 5cd5c1ffd6248..505f6853c7906 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -78,6 +78,7 @@ include::field-formatters/color-formatter.asciidoc[] [[scripted-fields]] === Scripted fields +deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default @@ -87,7 +88,7 @@ WARNING: Computing data on the fly with scripted fields can be very resource int {kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. -When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the +When you define a scripted field in {kib}, you have a choice of the {ref}/modules-scripting-expression.html[Lucene expressions] or the {ref}/modules-scripting-painless.html[Painless] scripting language. You can reference any single value numeric field in your expressions, for example: diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index f5ebac1ebf02e..acb343191609d 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -320,6 +320,15 @@ All supported operating systems support using systemd service files. Any system *Impact:* Any installations using `.deb` or `.rpm` packages using SysV will need to migrate to systemd. +[float] +=== TLS v1.0 and v1.1 are disabled by default + +*Details:* +Support can be re-enabled by setting `--tls-min-1.0` in the `node.options` config file that can be found inside `kibana/config` folder or any other configured with the environment variable `KBN_PATH_CONF` (for example in Debian based system would be `/etc/kibana`). + +*Impact:* +Browser and proxy clients communicating over TLS v1.0 and v1.1. + [float] === Platform removed from root folder name for `.tar.gz` and `.zip` archives diff --git a/docs/settings/banners-settings.asciidoc b/docs/settings/banners-settings.asciidoc index 2a68cbe82f9f2..ce56d4dbe7a4d 100644 --- a/docs/settings/banners-settings.asciidoc +++ b/docs/settings/banners-settings.asciidoc @@ -9,6 +9,11 @@ Banners are disabled by default. You need to manually configure them in order to You can configure the `xpack.banners` settings in your `kibana.yml` file. +[NOTE] +==== +Banners are a https://www.elastic.co/subscriptions[subscription feature]. +==== + [[general-banners-settings-kb]] ==== General banner settings @@ -16,7 +21,7 @@ You can configure the `xpack.banners` settings in your `kibana.yml` file. |=== | `xpack.banners.placement` -| Set to `header` to enable the header banner. Defaults to `disabled`. +| Set to `top` to display a banner above the Elastic header. Defaults to `disabled`. | `xpack.banners.textContent` | The text to display inside the banner, either plain text or Markdown. @@ -27,9 +32,7 @@ You can configure the `xpack.banners` settings in your `kibana.yml` file. | `xpack.banners.backgroundColor` | The color of the banner background. Defaults to `#FFF9E8`. -|=== +| `xpack.banners.disableSpaceBanners` +| If true, per-space banner overrides will be disabled. Defaults to `false`. -[NOTE] -==== -The `banners` plugin is a https://www.elastic.co/subscriptions[subscription feature] -==== \ No newline at end of file +|=== diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index cef5a953fded4..9bb11f3f99a15 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -260,19 +260,21 @@ For information about {kib} memory limits, see <> setting. Defaults to `.reporting`. - | `xpack.reporting.capture.networkPolicy` | Capturing a screenshot from a {kib} page involves sending out requests for all the linked web assets. For example, a Markdown visualization can show an image from a remote server. You can configure what type of requests to allow or filter by setting a <> for Reporting. +| `xpack.reporting.index` + | deprecated:[7.11.0,This setting 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 <> + setting. Defaults to `.reporting`. + | `xpack.reporting.roles.allow` | Specifies the roles in addition to superusers that can use reporting. Defaults to `[ "reporting_user" ]`. + diff --git a/docs/settings/search-sessions-settings.asciidoc b/docs/settings/search-sessions-settings.asciidoc index c9a9e709ac7f8..cf64d08e4806c 100644 --- a/docs/settings/search-sessions-settings.asciidoc +++ b/docs/settings/search-sessions-settings.asciidoc @@ -11,15 +11,15 @@ Configure the search session settings in your `kibana.yml` configuration file. [cols="2*<"] |=== a| `xpack.data_enhanced.` -`search.sessions:enabled` - | Set to `true` (default) to enable search sessions. +`search.sessions.enabled` +| Set to `true` (default) to enable search sessions. -a| `xpack.data.enhanced.` -`search.sessions:trackingInterval` - | The frequency for updating the state of a search session. The default is 10s. +a| `xpack.data_enhanced.` +`search.sessions.trackingInterval` +| The frequency for updating the state of a search session. The default is 10s. -a| `xpack.data.enhanced.` -`search.sessions:defaultExpiration` - | How long search session results are stored before they are deleted. - Extending a search session resets the expiration by the same value. The default is 7d. +a| `xpack.data_enhanced.` +`search.sessions.defaultExpiration` +| How long search session results are stored before they are deleted. +Extending a search session resets the expiration by the same value. The default is 7d. |=== diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index e5cbc2c7ea6db..73b268e1e48b3 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -25,12 +25,14 @@ which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* | `cpu.cgroup.path.override:` - | *deprecated* This setting has been renamed to <> -and the old name will no longer be supported as of 8.0. + | deprecated:[7.10.0,"This setting will no longer be supported as of 8.0."] + This setting has been renamed to + <>. | `cpuacct.cgroup.path.override:` - | *deprecated* This setting has been renamed to <> -and the old name will no longer be supported as of 8.0. + | deprecated:[7.10.0,"This setting will no longer be supported as of 8.0."] + This setting has been renamed to + <>. | `csp.rules:` | A https://w3c.github.io/webappsec-csp/[content-security-policy] template @@ -64,10 +66,12 @@ To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. | `elasticsearch.logQueries:` - | *deprecated* This setting is no longer used and will get removed in Kibana 8.0. Instead, configure the `elasticsearch.query` logger. -This is useful for seeing the query DSL generated by applications that -currently do not have an inspector, for example Timelion and Monitoring. -*Default: `false`* + | deprecated:[7.12.0,"This setting is no longer used and will be removed in Kibana 8.0."] + Instead, configure the `elasticsearch.query` logger. + + + This is useful for seeing the query DSL generated by applications that + currently do not have an inspector, for example Timelion and Monitoring. + *Default: `false`* The following example shows a valid `elasticsearch.query` logger configuration: |=== @@ -240,18 +244,22 @@ on the {kib} index at startup. {kib} users still need to authenticate with | Enables use of interpreter in Visualize. *Default: `true`* | `kibana.defaultAppId:` - | *deprecated* This setting is deprecated and will get removed in Kibana 8.0. -Please use the `defaultRoute` advanced setting instead. -The default application to load. *Default: `"home"`* + | deprecated:[7.9.0,This setting will be removed in Kibana 8.0.] + Instead, use the <>. + + + The default application to load. *Default: `"home"`* |[[kibana-index]] `kibana.index:` - | *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. {kib} uses an index in {es} to store saved searches, visualizations, and -dashboards. {kib} creates a new index if the index doesn’t already exist. -If you configure a custom index, the name must be lowercase, and conform to the -{es} {ref}/indices-create-index.html[index name limitations]. -*Default: `".kibana"`* + | deprecated:[7.11.0,This setting 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. + + + {kib} uses an index in {es} to store saved searches, visualizations, and + dashboards. {kib} creates a new index if the index doesn’t already exist. If + you configure a custom index, the name must be lowercase, and conform to the + {es} {ref}/indices-create-index.html[index name limitations]. + *Default: `".kibana"`* | `kibana.autocompleteTimeout:` {ess-icon} | Time in milliseconds to wait for autocomplete suggestions from {es}. diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 676c46368a6ee..80ce77f30c75e 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -4,17 +4,7 @@ Instead of using a visual editor to create charts, you define a graph by chaining functions together, using the *Timelion*-specific syntax. The syntax enables some features that classical point series charts don't offer, such as pulling data from different indices or data sources into one graph. -[NOTE] -==== -Timelion app deprecation - -*Timelion* is still supported, the *Timelion app* is deprecated in 7.0, replaced by -dashboard features. In 8.0 and later, the *Timelion app* is removed from {kib}. -To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. - -For information on how to migrate *Timelion app* worksheets, refer to the -link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]. -==== +deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In 8.0 and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."] [float] ==== Timelion expressions diff --git a/package.json b/package.json index e379123269847..05a8e450793d6 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "26.0.0", + "@elastic/charts": "26.1.0", "@elastic/datemath": "link:packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.12.0", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index f93849e011d41..3c9fd4f59a406 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -9,7 +9,7 @@ pageLoadAssetSize: charts: 195358 cloud: 21076 console: 46091 - core: 692106 + core: 397521 crossClusterReplication: 65408 dashboard: 374194 dashboardEnhanced: 65646 @@ -24,13 +24,13 @@ pageLoadAssetSize: enterpriseSearch: 35741 esUiShared: 326654 expressions: 224136 - features: 31211 - globalSearch: 43548 - globalSearchBar: 62888 + features: 21723 + globalSearch: 29696 + globalSearchBar: 50403 globalSearchProviders: 25554 graph: 31504 grokdebugger: 26779 - home: 41661 + home: 30182 indexLifecycleManagement: 107090 indexManagement: 140608 indexPatternManagement: 28222 @@ -45,11 +45,11 @@ pageLoadAssetSize: kibanaUtils: 198829 lens: 96624 licenseManagement: 41817 - licensing: 39008 + licensing: 29004 lists: 202261 logstash: 53548 management: 46112 - maps: 183610 + maps: 80000 mapsLegacy: 87859 mapsLegacyLicensing: 20214 ml: 82187 @@ -73,8 +73,8 @@ pageLoadAssetSize: share: 99061 snapshotRestore: 79032 spaces: 387915 - telemetry: 91832 - telemetryManagementSection: 52443 + telemetry: 51957 + telemetryManagementSection: 38586 tileMap: 65337 timelion: 29920 transform: 41007 @@ -108,3 +108,4 @@ pageLoadAssetSize: fileUpload: 25664 banners: 17946 mapsEms: 26072 + timelines: 28613 diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 6e3106dbc2af7..d5b9996dfb2cd 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import 'source-map-support/register'; - import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 0fa79fff6e0d9..050aadd402d8a 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -9,7 +9,7 @@ }, "scripts": { "build": "../../node_modules/.bin/webpack", - "kbn:watch": "../../node_modules/.bin/webpack --watch --progress", + "kbn:watch": "../../node_modules/.bin/webpack --watch", "prettier": "../../node_modules/.bin/prettier --write './src/**/*.ts'" }, "devDependencies": { diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index ede617908fd3d..f14c793d22a09 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -47,3 +47,5 @@ export const LodashFp = require('lodash/fp'); // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); export const KbnAnalytics = require('@kbn/analytics'); +export const KbnStd = require('@kbn/std'); +export const SaferLodashSet = require('@elastic/safer-lodash-set'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index d1217dd8db0d4..0542bc89ff9e4 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -58,5 +58,7 @@ exports.externals = { */ tslib: '__kbnSharedDeps__.TsLib', '@kbn/analytics': '__kbnSharedDeps__.KbnAnalytics', + '@kbn/std': '__kbnSharedDeps__.KbnStd', + '@elastic/safer-lodash-set': '__kbnSharedDeps__.SaferLodashSet', }; exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index 135884fbf13e7..76e6843bea2f8 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -177,22 +177,22 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ compiler.hooks.emit.tap('MetricsPlugin', (compilation) => { const metrics = [ { - group: '@kbn/ui-shared-deps asset size', - id: 'kbn-ui-shared-deps.js', + group: 'page load bundle size', + id: 'kbnUiSharedDeps-js', value: compilation.assets['kbn-ui-shared-deps.js'].size(), }, { - group: '@kbn/ui-shared-deps asset size', - id: 'kbn-ui-shared-deps.@elastic.js', - value: compilation.assets['kbn-ui-shared-deps.@elastic.js'].size(), - }, - { - group: '@kbn/ui-shared-deps asset size', - id: 'css', + group: 'page load bundle size', + id: 'kbnUiSharedDeps-css', value: compilation.assets['kbn-ui-shared-deps.css'].size() + compilation.assets['kbn-ui-shared-deps.v7.light.css'].size(), }, + { + group: 'page load bundle size', + id: 'kbnUiSharedDeps-elastic', + value: compilation.assets['kbn-ui-shared-deps.@elastic.js'].size(), + }, ]; compilation.emitAsset( diff --git a/scripts/build_kibana_platform_plugins.js b/scripts/build_kibana_platform_plugins.js index fa630e0bb1808..9038d08364400 100644 --- a/scripts/build_kibana_platform_plugins.js +++ b/scripts/build_kibana_platform_plugins.js @@ -7,6 +7,7 @@ */ require('../src/setup_node_env/ensure_node_preserve_symlinks'); +require('source-map-support/register'); require('@kbn/optimizer').runKbnOptimizerCli({ defaultLimitsPath: require.resolve('../packages/kbn-optimizer/limits.yml'), }); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index cd0ce7005cc41..6f1b9dc5bf820 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -52,6 +52,8 @@ export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_serv export { i18nServiceMock } from './i18n/i18n_service.mock'; export { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; +type MockedPluginInitializerConfig = jest.Mocked['config']>; + export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { kibana: { @@ -70,7 +72,7 @@ export function pluginInitializerContextConfigMock(config: T) { }, }; - const mock: jest.Mocked['config']> = { + const mock: MockedPluginInitializerConfig = { legacy: { globalConfig$: of(globalConfig), get: () => globalConfig, @@ -82,8 +84,12 @@ export function pluginInitializerContextConfigMock(config: T) { return mock; } +type PluginInitializerContextMock = Omit, 'config'> & { + config: MockedPluginInitializerConfig; +}; + function pluginInitializerContextMock(config: T = {} as T) { - const mock: PluginInitializerContext = { + const mock: PluginInitializerContextMock = { opaqueId: Symbol(), logger: loggingSystemMock.create(), env: { diff --git a/src/dev/build/tasks/bin/scripts/kibana b/src/dev/build/tasks/bin/scripts/kibana index 3c12c8bbf58d0..a4fc5385500b5 100755 --- a/src/dev/build/tasks/bin/scripts/kibana +++ b/src/dev/build/tasks/bin/scripts/kibana @@ -26,4 +26,4 @@ if [ -f "${CONFIG_DIR}/node.options" ]; then KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)" fi -NODE_OPTIONS="--no-warnings --max-http-header-size=65536 --tls-min-v1.0 $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli/dist" ${@} +NODE_OPTIONS="--no-warnings --max-http-header-size=65536 $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli/dist" ${@} diff --git a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap index 517a6238c2519..be5163e89367c 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/components/field/__snapshots__/field.test.tsx.snap @@ -1791,6 +1791,7 @@ exports[`Field for json setting should render as read only if saving is disabled maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -1897,6 +1898,7 @@ exports[`Field for json setting should render as read only with help text if ove maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -1979,6 +1981,7 @@ exports[`Field for json setting should render custom setting icon if it is custo maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2092,6 +2095,7 @@ exports[`Field for json setting should render default value if there is no user maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2181,6 +2185,7 @@ exports[`Field for json setting should render unsaved value if there are unsaved maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2305,6 +2310,7 @@ exports[`Field for json setting should render user value if there is user value maxLines={30} minLines={6} mode="json" + name="advancedSetting-editField-json:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2376,6 +2382,7 @@ exports[`Field for markdown setting should render as read only if saving is disa maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2479,6 +2486,7 @@ exports[`Field for markdown setting should render as read only with help text if maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2561,6 +2569,7 @@ exports[`Field for markdown setting should render custom setting icon if it is c maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2632,6 +2641,7 @@ exports[`Field for markdown setting should render default value if there is no u maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2721,6 +2731,7 @@ exports[`Field for markdown setting should render unsaved value if there are uns maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { @@ -2838,6 +2849,7 @@ exports[`Field for markdown setting should render user value if there is user va maxLines={30} minLines={6} mode="markdown" + name="advancedSetting-editField-markdown:test:setting-editor" onChange={[Function]} setOptions={ Object { diff --git a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx index f5db5c3e371b3..d4a5020bbbb82 100644 --- a/src/plugins/advanced_settings/public/management_app/components/field/field.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/field/field.tsx @@ -326,6 +326,7 @@ export class Field extends PureComponent {
{ coreStart = coreMock.createStart(); + capabilities = { + ...coreStart.application.capabilities, + visualize: { save: true }, + maps: { save: true }, + }; const containerOptions = { ExitFullScreenButton: () => null, @@ -83,7 +90,10 @@ beforeEach(async () => { }); test('Add to library is incompatible with Error Embeddables', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); const errorEmbeddable = new ErrorEmbeddable( 'Wow what an awful error', { id: ' 404' }, @@ -92,20 +102,37 @@ test('Add to library is incompatible with Error Embeddables', async () => { expect(await action.isCompatible({ embeddable: errorEmbeddable })).toBe(false); }); +test('Add to library is incompatible on visualize embeddable without visualize save permissions', async () => { + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities: { ...capabilities, visualize: { save: false } }, + }); + expect(await action.isCompatible({ embeddable })).toBe(false); +}); + test('Add to library is compatible when embeddable on dashboard has value type input', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Add to library is not compatible when embeddable input is by reference', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Add to library is not compatible when view mode is set to view', async () => { - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); @@ -126,7 +153,10 @@ test('Add to library is not compatible when embeddable is not in a dashboard con mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id }, mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id }, }); - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); @@ -135,7 +165,10 @@ test('Add to library replaces embeddableId and retains panel count', async () => const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); @@ -161,7 +194,10 @@ test('Add to library returns reference type input', async () => { }); const dashboard = embeddable.getRoot() as IContainer; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); + const action = new AddToLibraryAction({ + toasts: coreStart.notifications.toasts, + capabilities, + }); await action.execute({ embeddable }); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index ef730e16bc5cf..fa102a9415b3f 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -18,7 +18,7 @@ import { isReferenceOrValueEmbeddable, isErrorEmbeddable, } from '../../services/embeddable'; -import { NotificationsStart } from '../../services/core'; +import { ApplicationStart, NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; @@ -33,7 +33,12 @@ export class AddToLibraryAction implements Action { public readonly id = ACTION_ADD_TO_LIBRARY; public order = 15; - constructor(private deps: { toasts: NotificationsStart['toasts'] }) {} + constructor( + private deps: { + toasts: NotificationsStart['toasts']; + capabilities: ApplicationStart['capabilities']; + } + ) {} public getDisplayName({ embeddable }: AddToLibraryActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { @@ -50,8 +55,15 @@ export class AddToLibraryAction implements Action { } public async isCompatible({ embeddable }: AddToLibraryActionContext) { + // TODO: Fix this, potentially by adding a 'canSave' function to embeddable interface + const canSave = + embeddable.type === 'map' + ? this.deps.capabilities.maps?.save + : this.deps.capabilities.visualize.save; + return Boolean( - !isErrorEmbeddable(embeddable) && + canSave && + !isErrorEmbeddable(embeddable) && embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index c82f17f2b29c4..829344504b16b 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -61,7 +61,8 @@ export class ClonePanelAction implements Action { embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && - embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE + embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE && + embeddable.getOutput().editable ); } diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index ae2d2b5f237c9..5bf730996ab4f 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -342,7 +342,7 @@ export class DashboardPlugin } public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart { - const { notifications, overlays } = core; + const { notifications, overlays, application } = core; const { uiActions, data, share, presentationUtil, embeddable } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -370,7 +370,10 @@ export class DashboardPlugin } if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { - const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); + const addToLibraryAction = new AddToLibraryAction({ + toasts: notifications.toasts, + capabilities: application.capabilities, + }); uiActions.registerAction(addToLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); @@ -386,8 +389,8 @@ export class DashboardPlugin overlays, embeddable.getStateTransfer(), { - canCreateNew: Boolean(core.application.capabilities.dashboard.createNew), - canEditExisting: !Boolean(core.application.capabilities.dashboard.hideWriteControls), + canCreateNew: Boolean(application.capabilities.dashboard.createNew), + canEditExisting: !Boolean(application.capabilities.dashboard.hideWriteControls), }, presentationUtil.ContextProvider ); diff --git a/src/plugins/dashboard/public/services/core.ts b/src/plugins/dashboard/public/services/core.ts index 7c19b2d75a967..75461729841e9 100644 --- a/src/plugins/dashboard/public/services/core.ts +++ b/src/plugins/dashboard/public/services/core.ts @@ -12,4 +12,5 @@ export { PluginInitializerContext, ScopedHistory, NotificationsStart, + ApplicationStart, } from '../../../../core/public'; diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 0e9cf6aeb1f2f..283d276a22904 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -439,10 +439,14 @@ export class AggConfig { } fieldIsTimeField() { - const indexPattern = this.getIndexPattern(); - if (!indexPattern) return false; - const timeFieldName = indexPattern.timeFieldName; - return timeFieldName && this.fieldName() === timeFieldName; + const defaultTimeField = this.getIndexPattern()?.getTimeField?.()?.name; + const defaultTimeFields = defaultTimeField ? [defaultTimeField] : []; + const allTimeFields = + this.aggConfigs.timeFields && this.aggConfigs.timeFields.length > 0 + ? this.aggConfigs.timeFields + : defaultTimeFields; + const currentFieldName = this.fieldName(); + return allTimeFields.includes(currentFieldName); } public get type() { diff --git a/src/plugins/data/common/search/aggs/agg_configs.ts b/src/plugins/data/common/search/aggs/agg_configs.ts index 03c702aa72fb5..4d5d49754387d 100644 --- a/src/plugins/data/common/search/aggs/agg_configs.ts +++ b/src/plugins/data/common/search/aggs/agg_configs.ts @@ -64,6 +64,7 @@ export type IAggConfigs = AggConfigs; export class AggConfigs { public indexPattern: IndexPattern; public timeRange?: TimeRange; + public timeFields?: string[]; private readonly typesRegistry: AggTypesRegistryStart; aggs: IAggConfig[]; @@ -83,6 +84,10 @@ export class AggConfigs { configStates.forEach((params: any) => this.createAggConfig(params)); } + setTimeFields(timeFields: string[] | undefined) { + this.timeFields = timeFields; + } + setTimeRange(timeRange: TimeRange) { this.timeRange = timeRange; diff --git a/src/plugins/data/common/search/aggs/agg_types.ts b/src/plugins/data/common/search/aggs/agg_types.ts index d02f8e1fc5af4..1db60db507f0f 100644 --- a/src/plugins/data/common/search/aggs/agg_types.ts +++ b/src/plugins/data/common/search/aggs/agg_types.ts @@ -29,6 +29,7 @@ export const getAggTypes = () => ({ { name: METRIC_TYPES.AVG, fn: metrics.getAvgMetricAgg }, { name: METRIC_TYPES.SUM, fn: metrics.getSumMetricAgg }, { name: METRIC_TYPES.MEDIAN, fn: metrics.getMedianMetricAgg }, + { name: METRIC_TYPES.SINGLE_PERCENTILE, fn: metrics.getSinglePercentileMetricAgg }, { name: METRIC_TYPES.MIN, fn: metrics.getMinMetricAgg }, { name: METRIC_TYPES.MAX, fn: metrics.getMaxMetricAgg }, { name: METRIC_TYPES.STD_DEV, fn: metrics.getStdDeviationMetricAgg }, @@ -90,6 +91,7 @@ export const getAggTypesFunctions = () => [ metrics.aggGeoCentroid, metrics.aggMax, metrics.aggMedian, + metrics.aggSinglePercentile, metrics.aggMin, metrics.aggMovingAvg, metrics.aggPercentileRanks, diff --git a/src/plugins/data/common/search/aggs/aggs_service.test.ts b/src/plugins/data/common/search/aggs/aggs_service.test.ts index bba67640890ad..3f434b0cc1c15 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.test.ts @@ -82,6 +82,7 @@ describe('Aggs service', () => { "avg", "sum", "median", + "single_percentile", "min", "max", "std_dev", @@ -128,6 +129,7 @@ describe('Aggs service', () => { "avg", "sum", "median", + "single_percentile", "min", "max", "std_dev", @@ -194,9 +196,8 @@ describe('Aggs service', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(5); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); - expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); expect(start).toHaveProperty('datatableUtilities'); diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index c5543e5037fc6..86bda5019a496 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -17,7 +17,6 @@ import { getCalculateAutoTimeExpression, } from './'; import { AggsCommonSetup, AggsCommonStart } from './types'; -import { getDateMetaByDatatableColumn } from './utils/time_column_meta'; import { getDatatableColumnUtilities } from './utils/datatable_column_meta'; /** @internal */ @@ -89,12 +88,6 @@ export class AggsCommonService { return { calculateAutoTimeExpression, - getDateMetaByDatatableColumn: getDateMetaByDatatableColumn({ - calculateAutoTimeExpression, - getIndexPattern, - getConfig, - isDefaultTimezone, - }), datatableUtilities: getDatatableColumnUtilities({ getIndexPattern, createAggConfigs, diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts index 61ad66d7efdc9..4a83ae38d34db 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts @@ -64,7 +64,9 @@ export interface AggParamsDateHistogram extends BaseAggParams { useNormalizedEsInterval?: boolean; scaleMetricValues?: boolean; interval?: string; + used_interval?: string; time_zone?: string; + used_time_zone?: string; drop_partials?: boolean; format?: string; min_doc_count?: number; @@ -220,6 +222,21 @@ export const getDateHistogramBucketAgg = ({ } }, }, + { + name: 'used_interval', + default: autoInterval, + shouldShow() { + return false; + }, + write: () => {}, + serialize(val, agg) { + if (!agg) return undefined; + const { useNormalizedEsInterval } = agg.params; + const interval = agg.buckets.getInterval(useNormalizedEsInterval); + return interval.expression; + }, + toExpressionAst: () => undefined, + }, { name: 'time_zone', default: undefined, @@ -232,6 +249,18 @@ export const getDateHistogramBucketAgg = ({ output.params.time_zone = tz; }, }, + { + name: 'used_timezone', + shouldShow() { + return false; + }, + write: () => {}, + serialize(val, agg) { + if (!agg) return undefined; + return inferTimeZone(agg.params, agg.getIndexPattern(), isDefaultTimezone, getConfig); + }, + toExpressionAst: () => undefined, + }, { name: 'drop_partials', default: false, diff --git a/src/plugins/data/common/search/aggs/metrics/__snapshots__/single_percentile.test.ts.snap b/src/plugins/data/common/search/aggs/metrics/__snapshots__/single_percentile.test.ts.snap new file mode 100644 index 0000000000000..a8d546973b185 --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/__snapshots__/single_percentile.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AggTypeMetricSinglePercentileProvider class supports scripted fields 1`] = ` +Object { + "single_percentile": Object { + "percentiles": Object { + "percents": Array [ + 95, + ], + "script": Object { + "lang": undefined, + "source": "return 456", + }, + }, + }, +} +`; diff --git a/src/plugins/data/common/search/aggs/metrics/index.ts b/src/plugins/data/common/search/aggs/metrics/index.ts index 7038673d5d7c4..d37b74a1a28ae 100644 --- a/src/plugins/data/common/search/aggs/metrics/index.ts +++ b/src/plugins/data/common/search/aggs/metrics/index.ts @@ -36,6 +36,8 @@ export * from './max_fn'; export * from './max'; export * from './median_fn'; export * from './median'; +export * from './single_percentile_fn'; +export * from './single_percentile'; export * from './metric_agg_type'; export * from './metric_agg_types'; export * from './min_fn'; diff --git a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index d51038d8a15e8..ac2beaf574256 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -22,6 +22,7 @@ const metricAggFilter = [ '!geo_bounds', '!geo_centroid', '!filtered_metric', + '!single_percentile', ]; export const parentPipelineType = i18n.translate( diff --git a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index c0d1be4f47f9b..2564fcb7a002b 100644 --- a/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/common/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -28,6 +28,7 @@ const metricAggFilter: string[] = [ '!geo_bounds', '!geo_centroid', '!filtered_metric', + '!single_percentile', ]; const bucketAggFilter: string[] = []; diff --git a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts index 3b6c9d8a0d55d..a308153b3816b 100644 --- a/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts +++ b/src/plugins/data/common/search/aggs/metrics/metric_agg_types.ts @@ -20,6 +20,7 @@ export enum METRIC_TYPES { GEO_BOUNDS = 'geo_bounds', GEO_CENTROID = 'geo_centroid', MEDIAN = 'median', + SINGLE_PERCENTILE = 'single_percentile', MIN = 'min', MAX = 'max', MOVING_FN = 'moving_avg', diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts new file mode 100644 index 0000000000000..c2ba6ee1a403a --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { AggConfigs, IAggConfigs } from '../agg_configs'; +import { mockAggTypesRegistry } from '../test_helpers'; +import { METRIC_TYPES } from './metric_agg_types'; + +describe('AggTypeMetricSinglePercentileProvider class', () => { + let aggConfigs: IAggConfigs; + + beforeEach(() => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: 'bytes', + }; + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: METRIC_TYPES.SINGLE_PERCENTILE, + type: METRIC_TYPES.SINGLE_PERCENTILE, + schema: 'metric', + params: { + field: 'bytes', + percentile: 95, + }, + }, + ], + { + typesRegistry, + } + ); + }); + + it('requests the percentiles aggregation in the Elasticsearch query DSL', () => { + const dsl: Record = aggConfigs.toDsl(); + + expect(dsl.single_percentile.percentiles.field).toEqual('bytes'); + expect(dsl.single_percentile.percentiles.percents).toEqual([95]); + }); + + it('points to right value within multi metric for value bucket path', () => { + expect(aggConfigs.byId(METRIC_TYPES.SINGLE_PERCENTILE)!.getValueBucketPath()).toEqual( + `${METRIC_TYPES.SINGLE_PERCENTILE}.95` + ); + }); + + it('converts the response', () => { + const agg = aggConfigs.getResponseAggs()[0]; + + expect( + agg.getValue({ + [agg.id]: { + values: { + '95.0': 123, + }, + }, + }) + ).toEqual(123); + }); + + it('produces the expected expression ast', () => { + const agg = aggConfigs.getResponseAggs()[0]; + expect(agg.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "field": Array [ + "bytes", + ], + "id": Array [ + "single_percentile", + ], + "percentile": Array [ + 95, + ], + "schema": Array [ + "metric", + ], + }, + "function": "aggSinglePercentile", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + it('supports scripted fields', () => { + const typesRegistry = mockAggTypesRegistry(); + const field = { + name: 'bytes', + scripted: true, + language: 'painless', + script: 'return 456', + }; + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: METRIC_TYPES.SINGLE_PERCENTILE, + type: METRIC_TYPES.SINGLE_PERCENTILE, + schema: 'metric', + params: { + field: 'bytes', + percentile: 95, + }, + }, + ], + { + typesRegistry, + } + ); + + expect(aggConfigs.toDsl()).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile.ts new file mode 100644 index 0000000000000..4bdafcae327cd --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { aggSinglePercentileFnName } from './single_percentile_fn'; +import { MetricAggType } from './metric_agg_type'; +import { METRIC_TYPES } from './metric_agg_types'; +import { KBN_FIELD_TYPES } from '../../../../common'; +import { BaseAggParams } from '../types'; + +const singlePercentileTitle = i18n.translate('data.search.aggs.metrics.singlePercentileTitle', { + defaultMessage: 'Percentile', +}); + +export interface AggParamsSinglePercentile extends BaseAggParams { + field: string; + percentile: number; +} + +export const getSinglePercentileMetricAgg = () => { + return new MetricAggType({ + name: METRIC_TYPES.SINGLE_PERCENTILE, + expressionName: aggSinglePercentileFnName, + dslName: 'percentiles', + title: singlePercentileTitle, + valueType: 'number', + makeLabel(aggConfig) { + return i18n.translate('data.search.aggs.metrics.singlePercentileLabel', { + defaultMessage: 'Percentile {field}', + values: { field: aggConfig.getFieldDisplayName() }, + }); + }, + getValueBucketPath(aggConfig) { + return `${aggConfig.id}.${aggConfig.params.percentile}`; + }, + params: [ + { + name: 'field', + type: 'field', + filterFieldTypes: [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.DATE, KBN_FIELD_TYPES.HISTOGRAM], + }, + { + name: 'percentile', + default: 95, + write: (agg, output) => { + output.params.percents = [agg.params.percentile]; + }, + }, + ], + getValue(agg, bucket) { + let valueKey = String(agg.params.percentile); + if (Number.isInteger(agg.params.percentile)) { + valueKey += '.0'; + } + return bucket[agg.id].values[valueKey]; + }, + }); +}; diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile_fn.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile_fn.ts new file mode 100644 index 0000000000000..e7ef22c6faeee --- /dev/null +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile_fn.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; + +export const aggSinglePercentileFnName = 'aggSinglePercentile'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition< + typeof aggSinglePercentileFnName, + Input, + AggArgs, + Output +>; + +export const aggSinglePercentile = (): FunctionDefinition => ({ + name: aggSinglePercentileFnName, + help: i18n.translate('data.search.aggs.function.metrics.singlePercentile.help', { + defaultMessage: 'Generates a serialized agg config for a single percentile agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.metrics.singlePercentile.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.metrics.singlePercentile.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + percentile: { + types: ['number'], + required: true, + help: i18n.translate('data.search.aggs.metrics.singlePercentile.percentile.help', { + defaultMessage: 'Percentile to fetch', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + customLabel: { + types: ['string'], + help: i18n.translate('data.search.aggs.metrics.singlePercentile.customLabel.help', { + defaultMessage: 'Represents a custom label for this aggregation', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: METRIC_TYPES.SINGLE_PERCENTILE, + params: { + ...rest, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index e57410962fc08..675be2323b93e 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -9,7 +9,6 @@ import { Assign } from '@kbn/utility-types'; import { DatatableColumn } from 'src/plugins/expressions'; import { IndexPattern } from '../../index_patterns/index_patterns/index_pattern'; -import { TimeRange } from '../../query'; import { aggAvg, aggBucketAvg, @@ -57,6 +56,7 @@ import { AggParamsIpRange, AggParamsMax, AggParamsMedian, + AggParamsSinglePercentile, AggParamsMin, AggParamsMovingAvg, AggParamsPercentileRanks, @@ -86,6 +86,7 @@ import { METRIC_TYPES, AggConfig, aggFilteredMetric, + aggSinglePercentile, } from './'; export { IAggConfig, AggConfigSerialized } from './agg_config'; @@ -106,19 +107,6 @@ export interface AggsCommonSetup { /** @internal */ export interface AggsCommonStart { calculateAutoTimeExpression: ReturnType; - /** - * Helper function returning meta data about use date intervals for a data table column. - * If the column is not a column created by a date histogram aggregation of the esaggs data source, - * this function will return undefined. - * - * Otherwise, it will return the following attributes in an object: - * * `timeZone` time zone used to create the buckets (important e.g. for DST), - * * `timeRange` total time range of the fetch data (to infer partial buckets at the beginning and end of the data) - * * `interval` Interval used on elasticsearch (`auto` resolved to the actual interval) - */ - getDateMetaByDatatableColumn: ( - column: DatatableColumn - ) => Promise; datatableUtilities: { getIndexPattern: (column: DatatableColumn) => Promise; getAggConfig: (column: DatatableColumn) => Promise; @@ -183,6 +171,7 @@ export interface AggParamsMapping { [METRIC_TYPES.GEO_CENTROID]: AggParamsGeoCentroid; [METRIC_TYPES.MAX]: AggParamsMax; [METRIC_TYPES.MEDIAN]: AggParamsMedian; + [METRIC_TYPES.SINGLE_PERCENTILE]: AggParamsSinglePercentile; [METRIC_TYPES.MIN]: AggParamsMin; [METRIC_TYPES.STD_DEV]: AggParamsStdDeviation; [METRIC_TYPES.SUM]: AggParamsSum; @@ -229,6 +218,7 @@ export interface AggFunctionsMapping { aggGeoCentroid: ReturnType; aggMax: ReturnType; aggMedian: ReturnType; + aggSinglePercentile: ReturnType; aggMin: ReturnType; aggMovingAvg: ReturnType; aggPercentileRanks: ReturnType; diff --git a/src/plugins/data/common/search/aggs/utils/get_date_histogram_meta.ts b/src/plugins/data/common/search/aggs/utils/get_date_histogram_meta.ts new file mode 100644 index 0000000000000..0a3aab6286d89 --- /dev/null +++ b/src/plugins/data/common/search/aggs/utils/get_date_histogram_meta.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DatatableColumn } from 'src/plugins/expressions/common'; +import { TimeRange } from '../../../types'; +import type { AggParamsDateHistogram } from '../buckets'; +import { BUCKET_TYPES } from '../buckets/bucket_agg_types'; + +/** + * Helper function returning the used interval, used time zone and applied time filters for data table column created by the date_histogramm agg type. + * "auto" will get expanded to the actually used interval. + * If the column is not a column created by a date_histogram aggregation of the esaggs data source, + * this function will return undefined. + */ +export const getDateHistogramMetaDataByDatatableColumn = (column: DatatableColumn) => { + if (column.meta.source !== 'esaggs') return; + if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) return; + const params = (column.meta.sourceParams.params as unknown) as AggParamsDateHistogram; + + let interval: string | undefined; + if (params.used_interval && params.used_interval !== 'auto') { + interval = params.used_interval; + } + + return { + interval, + timeZone: params.used_time_zone, + timeRange: column.meta.sourceParams.appliedTimeRange as TimeRange | undefined, + }; +}; diff --git a/src/plugins/data/common/search/aggs/utils/index.ts b/src/plugins/data/common/search/aggs/utils/index.ts index f90e8f88546f4..c92653e843233 100644 --- a/src/plugins/data/common/search/aggs/utils/index.ts +++ b/src/plugins/data/common/search/aggs/utils/index.ts @@ -8,6 +8,7 @@ export * from './calculate_auto_time_expression'; export { getNumberHistogramIntervalByDatatableColumn } from './get_number_histogram_interval'; +export { getDateHistogramMetaDataByDatatableColumn } from './get_date_histogram_meta'; export * from './date_interval_utils'; export * from './get_format_with_aggs'; export * from './ipv4_address'; diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts deleted file mode 100644 index d9527ecd92a12..0000000000000 --- a/src/plugins/data/common/search/aggs/utils/time_column_meta.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { BUCKET_TYPES } from '../buckets'; -import { DateMetaByColumnDeps, getDateMetaByDatatableColumn } from './time_column_meta'; - -describe('getDateMetaByDatatableColumn', () => { - let params: DateMetaByColumnDeps; - beforeEach(() => { - params = { - calculateAutoTimeExpression: jest.fn().mockReturnValue('5m'), - getIndexPattern: jest.fn().mockResolvedValue({}), - isDefaultTimezone: jest.fn().mockReturnValue(true), - getConfig: jest.fn(), - }; - }); - - it('returns nothing on column from other data source', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'essql', - }, - }) - ).toEqual(undefined); - }); - - it('returns nothing on non date histogram column', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.TERMS, - }, - }, - }) - ).toEqual(undefined); - }); - - it('returns time range, time zone and interval', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - interval: '1h', - }, - appliedTimeRange: { - from: 'now-5d', - to: 'now', - }, - }, - }, - }) - ).toEqual({ - timeZone: 'UTC', - timeRange: { - from: 'now-5d', - to: 'now', - }, - interval: '1h', - }); - }); - - it('throws if unable to resolve interval', async () => { - await expect( - getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - interval: 'auto', - }, - }, - }, - }) - ).rejects.toBeDefined(); - - await expect( - getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - }, - }, - }, - }) - ).rejects.toBeDefined(); - }); - - it('returns resolved auto interval', async () => { - expect( - await getDateMetaByDatatableColumn(params)({ - id: 'test', - name: 'test', - meta: { - type: 'date', - source: 'esaggs', - sourceParams: { - type: BUCKET_TYPES.DATE_HISTOGRAM, - params: { - time_zone: 'UTC', - interval: 'auto', - }, - appliedTimeRange: { - from: '2020-10-05T00:00:00.000Z', - to: '2020-10-10T00:00:00.000Z', - }, - }, - }, - }) - ).toEqual( - expect.objectContaining({ - interval: '5m', - }) - ); - }); -}); diff --git a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts b/src/plugins/data/common/search/aggs/utils/time_column_meta.ts deleted file mode 100644 index b0912803908cc..0000000000000 --- a/src/plugins/data/common/search/aggs/utils/time_column_meta.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { DatatableColumn } from 'src/plugins/expressions/common'; -import { IndexPattern } from '../../../index_patterns'; - -import { TimeRange } from '../../../types'; -import { AggParamsDateHistogram, BUCKET_TYPES } from '../buckets'; -import { inferTimeZone } from './infer_time_zone'; - -export interface DateMetaByColumnDeps { - calculateAutoTimeExpression: (range: TimeRange) => string | undefined; - getIndexPattern: (id: string) => Promise; - isDefaultTimezone: () => boolean; - getConfig: (key: string) => T; -} - -export const getDateMetaByDatatableColumn = ({ - calculateAutoTimeExpression, - getIndexPattern, - isDefaultTimezone, - getConfig, -}: DateMetaByColumnDeps) => async ( - column: DatatableColumn -): Promise => { - if (column.meta.source !== 'esaggs') return; - if (column.meta.sourceParams?.type !== BUCKET_TYPES.DATE_HISTOGRAM) return; - const params = column.meta.sourceParams.params as AggParamsDateHistogram; - const appliedTimeRange = column.meta.sourceParams.appliedTimeRange as TimeRange | undefined; - - const tz = inferTimeZone( - params, - await getIndexPattern(column.meta.sourceParams.indexPatternId as string), - isDefaultTimezone, - getConfig - ); - - const interval = - params.interval === 'auto' && appliedTimeRange - ? calculateAutoTimeExpression(appliedTimeRange) - : params.interval; - - if (!interval || interval === 'auto') { - throw new Error('time interval could not be determined'); - } - - return { - timeZone: tz, - timeRange: appliedTimeRange, - interval, - }; -}; diff --git a/src/plugins/data/public/search/expressions/__snapshots__/es_raw_response.test.ts.snap b/src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap similarity index 100% rename from src/plugins/data/public/search/expressions/__snapshots__/es_raw_response.test.ts.snap rename to src/plugins/data/common/search/expressions/__snapshots__/es_raw_response.test.ts.snap diff --git a/src/plugins/data/public/search/expressions/es_raw_response.test.ts b/src/plugins/data/common/search/expressions/es_raw_response.test.ts similarity index 100% rename from src/plugins/data/public/search/expressions/es_raw_response.test.ts rename to src/plugins/data/common/search/expressions/es_raw_response.test.ts diff --git a/src/plugins/data/public/search/expressions/es_raw_response.ts b/src/plugins/data/common/search/expressions/es_raw_response.ts similarity index 100% rename from src/plugins/data/public/search/expressions/es_raw_response.ts rename to src/plugins/data/common/search/expressions/es_raw_response.ts diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts index f8604266cd6e0..7580032b0dd85 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.test.ts @@ -33,6 +33,7 @@ describe('esaggs expression function - public', () => { setTimeRange: jest.fn(), toDsl: jest.fn().mockReturnValue({ aggs: {} }), onSearchRequestStart: jest.fn(), + setTimeFields: jest.fn(), } as unknown) as jest.Mocked, filters: undefined, indexPattern: ({ id: 'logstash-*' } as unknown) as jest.Mocked, diff --git a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts index e2ee1a31757cb..72d9cc4095570 100644 --- a/src/plugins/data/common/search/expressions/esaggs/request_handler.ts +++ b/src/plugins/data/common/search/expressions/esaggs/request_handler.ts @@ -73,6 +73,7 @@ export const handleRequest = async ({ const requestSearchSource = timeFilterSearchSource.createChild({ callParentStartHandlers: true }); aggs.setTimeRange(timeRange as TimeRange); + aggs.setTimeFields(timeFields); // For now we need to mirror the history of the passed search source, since // the request inspector wouldn't work otherwise. diff --git a/src/plugins/data/common/search/expressions/esdsl.ts b/src/plugins/data/common/search/expressions/esdsl.ts new file mode 100644 index 0000000000000..dee1b19eb3360 --- /dev/null +++ b/src/plugins/data/common/search/expressions/esdsl.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; + +import { EsRawResponse } from './es_raw_response'; +import { RequestStatistics, RequestAdapter } from '../../../../inspector/common'; +import { ISearchGeneric, KibanaContext } from '..'; +import { buildEsQuery, getEsQueryConfig } from '../../es_query/es_query'; +import { UiSettingsCommon } from '../../index_patterns'; + +const name = 'esdsl'; + +type Input = KibanaContext | null; +type Output = Promise; + +interface Arguments { + dsl: string; + index: string; + size: number; +} + +export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof name, + Input, + Arguments, + Output +>; + +/** @internal */ +export interface EsdslStartDependencies { + search: ISearchGeneric; + uiSettingsClient: UiSettingsCommon; +} + +export const getEsdslFn = ({ + getStartDependencies, +}: { + getStartDependencies: (getKibanaRequest: any) => Promise; +}) => { + const esdsl: EsdslExpressionFunctionDefinition = { + name, + type: 'es_raw_response', + inputTypes: ['kibana_context', 'null'], + help: i18n.translate('data.search.esdsl.help', { + defaultMessage: 'Run Elasticsearch request', + }), + args: { + dsl: { + types: ['string'], + aliases: ['_', 'q', 'query'], + help: i18n.translate('data.search.esdsl.q.help', { + defaultMessage: 'Query DSL', + }), + required: true, + }, + index: { + types: ['string'], + help: i18n.translate('data.search.esdsl.index.help', { + defaultMessage: 'ElasticSearch index to query', + }), + required: true, + }, + size: { + types: ['number'], + help: i18n.translate('data.search.esdsl.size.help', { + defaultMessage: 'ElasticSearch searchAPI size parameter', + }), + default: 10, + }, + }, + async fn(input, args, { inspectorAdapters, abortSignal, getKibanaRequest }) { + const { search, uiSettingsClient } = await getStartDependencies(getKibanaRequest); + + const dsl = JSON.parse(args.dsl); + + if (input) { + const esQueryConfigs = getEsQueryConfig(uiSettingsClient as any); + const query = buildEsQuery( + undefined, // args.index, + input.query || [], + input.filters || [], + esQueryConfigs + ); + + if (dsl.query) { + query.bool.must.push(dsl.query); + } + + dsl.query = query; + } + + if (!inspectorAdapters.requests) { + inspectorAdapters.requests = new RequestAdapter(); + } + + const request = inspectorAdapters.requests.start( + i18n.translate('data.search.dataRequest.title', { + defaultMessage: 'Data', + }), + { + description: i18n.translate('data.search.es_search.dataRequest.description', { + defaultMessage: + 'This request queries Elasticsearch to fetch the data for the visualization.', + }), + } + ); + + request.stats({ + indexPattern: { + label: i18n.translate('data.search.es_search.indexPatternLabel', { + defaultMessage: 'Index pattern', + }), + value: args.index, + description: i18n.translate('data.search.es_search.indexPatternDescription', { + defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', + }), + }, + }); + + try { + const { rawResponse } = await search( + { + params: { + index: args.index, + size: args.size, + body: dsl, + }, + }, + { abortSignal } + ).toPromise(); + + const stats: RequestStatistics = {}; + + if (rawResponse?.took) { + stats.queryTime = { + label: i18n.translate('data.search.es_search.queryTimeLabel', { + defaultMessage: 'Query time', + }), + value: i18n.translate('data.search.es_search.queryTimeValue', { + defaultMessage: '{queryTime}ms', + values: { queryTime: rawResponse.took }, + }), + description: i18n.translate('data.search.es_search.queryTimeDescription', { + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', + }), + }; + } + + if (rawResponse?.hits) { + stats.hitsTotal = { + label: i18n.translate('data.search.es_search.hitsTotalLabel', { + defaultMessage: 'Hits (total)', + }), + value: `${rawResponse.hits.total}`, + description: i18n.translate('data.search.es_search.hitsTotalDescription', { + defaultMessage: 'The number of documents that match the query.', + }), + }; + + stats.hits = { + label: i18n.translate('data.search.es_search.hitsLabel', { + defaultMessage: 'Hits', + }), + value: `${rawResponse.hits.hits.length}`, + description: i18n.translate('data.search.es_search.hitsDescription', { + defaultMessage: 'The number of documents returned by the query.', + }), + }; + } + + request.stats(stats).ok({ json: rawResponse }); + request.json(dsl); + + return { + type: 'es_raw_response', + body: rawResponse, + }; + } catch (e) { + request.error({ json: e }); + throw e; + } + }, + }; + return esdsl; +}; diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index b80cbad778a11..6df6dacddeb2d 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -23,3 +23,5 @@ export * from './range_filter'; export * from './kibana_filter'; export * from './filters_to_ast'; export * from './timerange'; +export * from './es_raw_response'; +export * from './esdsl'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 1838ca43e8c23..c47cd6cd9740d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -313,6 +313,7 @@ import { toAbsoluteDates, boundsDescendingRaw, getNumberHistogramIntervalByDatatableColumn, + getDateHistogramMetaDataByDatatableColumn, // expressions utils getRequestInspectorStats, getResponseInspectorStats, @@ -423,6 +424,7 @@ export const search = { toAbsoluteDates, boundsDescendingRaw, getNumberHistogramIntervalByDatatableColumn, + getDateHistogramMetaDataByDatatableColumn, }, getRequestInspectorStats, getResponseInspectorStats, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 746d035e9bfb6..7f243cefd08b6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -40,7 +40,6 @@ import { EventEmitter } from 'events'; import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { ExpressionFunctionDefinition as ExpressionFunctionDefinition_2 } from 'src/plugins/expressions/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { ExpressionValueBoxed } from 'src/plugins/expressions/common'; import { FormatFactory as FormatFactory_2 } from 'src/plugins/data/common/field_formats/utils'; @@ -132,7 +131,7 @@ export class AggConfig { enabled: boolean; static ensureIds(list: any[]): any[]; // (undocumented) - fieldIsTimeField(): boolean | "" | undefined; + fieldIsTimeField(): boolean; // (undocumented) fieldName(): any; // (undocumented) @@ -260,8 +259,12 @@ export class AggConfigs { // (undocumented) onSearchRequestStart(searchSource: ISearchSource_2, options?: ISearchOptions_2): Promise<[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]>; // (undocumented) + setTimeFields(timeFields: string[] | undefined): void; + // (undocumented) setTimeRange(timeRange: TimeRange): void; // (undocumented) + timeFields?: string[]; + // (undocumented) timeRange?: TimeRange; // (undocumented) toDsl(hierarchical?: boolean): Record; @@ -396,6 +399,10 @@ export interface AggFunctionsMapping { // // (undocumented) aggSignificantTerms: ReturnType; + // Warning: (ae-forgotten-export) The symbol "aggSinglePercentile" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggSinglePercentile: ReturnType; // Warning: (ae-forgotten-export) The symbol "aggStdDeviation" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -714,7 +721,7 @@ export const ES_SEARCH_STRATEGY = "es"; // Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>; +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_36, Arguments_21, Output_36>; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Input" needs to be exported by the entry point index.d.ts @@ -723,7 +730,7 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'e // Warning: (ae-missing-release-tag) "EsdslExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2; +export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition; // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1857,6 +1864,8 @@ export enum METRIC_TYPES { // (undocumented) SERIAL_DIFF = "serial_diff", // (undocumented) + SINGLE_PERCENTILE = "single_percentile", + // (undocumented) STD_DEV = "std_dev", // (undocumented) SUM = "sum", @@ -2240,7 +2249,7 @@ export const search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; @@ -2273,6 +2282,11 @@ export const search: { intervalLabel: string; })[]; getNumberHistogramIntervalByDatatableColumn: (column: import("../../expressions").DatatableColumn) => number | undefined; + getDateHistogramMetaDataByDatatableColumn: (column: import("../../expressions").DatatableColumn) => { + interval: string | undefined; + timeZone: string | undefined; + timeRange: import("../common").TimeRange | undefined; + } | undefined; }; getRequestInspectorStats: typeof getRequestInspectorStats; getResponseInspectorStats: typeof getResponseInspectorStats; @@ -2642,7 +2656,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:169:7 - (ae-forgotten-export) The symbol "RuntimeField" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/search/aggs/types.ts:141:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/aggs/types.ts:129:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:56:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:56:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts @@ -2676,21 +2690,21 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:430:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:406:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:428:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:431:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/search/session/session_service.ts:56:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index 7e9170b98f132..cd2ee69d33996 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -54,7 +54,7 @@ describe('AggsService - public', () => { service.setup(setupDeps); const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(11); - expect(start.types.getAll().metrics.length).toBe(22); + expect(start.types.getAll().metrics.length).toBe(23); }); test('registers custom agg types', () => { @@ -71,7 +71,7 @@ describe('AggsService - public', () => { const start = service.start(startDeps); expect(start.types.getAll().buckets.length).toBe(12); expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); - expect(start.types.getAll().metrics.length).toBe(23); + expect(start.types.getAll().metrics.length).toBe(24); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true); }); }); @@ -79,9 +79,8 @@ describe('AggsService - public', () => { describe('start()', () => { test('exposes proper contract', () => { const start = service.start(startDeps); - expect(Object.keys(start).length).toBe(5); + expect(Object.keys(start).length).toBe(4); expect(start).toHaveProperty('calculateAutoTimeExpression'); - expect(start).toHaveProperty('getDateMetaByDatatableColumn'); expect(start).toHaveProperty('createAggConfigs'); expect(start).toHaveProperty('types'); expect(start).toHaveProperty('datatableUtilities'); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 26f19ca7eaef4..f603bd733f601 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -91,16 +91,13 @@ export class AggsService { public start({ fieldFormats, uiSettings, indexPatterns }: AggsStartDependencies): AggsStart { const isDefaultTimezone = () => uiSettings.isDefault('dateFormat:tz'); - const { - calculateAutoTimeExpression, - getDateMetaByDatatableColumn, - datatableUtilities, - types, - } = this.aggsCommonService.start({ - getConfig: this.getConfig!, - getIndexPattern: indexPatterns.get, - isDefaultTimezone, - }); + const { calculateAutoTimeExpression, datatableUtilities, types } = this.aggsCommonService.start( + { + getConfig: this.getConfig!, + getIndexPattern: indexPatterns.get, + isDefaultTimezone, + } + ); const aggTypesDependencies: AggTypesDependencies = { calculateBounds: this.calculateBounds, @@ -140,7 +137,6 @@ export class AggsService { return { calculateAutoTimeExpression, - getDateMetaByDatatableColumn, datatableUtilities, createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); diff --git a/src/plugins/data/public/search/aggs/mocks.ts b/src/plugins/data/public/search/aggs/mocks.ts index 9c90af801bdac..fb50058f08348 100644 --- a/src/plugins/data/public/search/aggs/mocks.ts +++ b/src/plugins/data/public/search/aggs/mocks.ts @@ -56,7 +56,6 @@ export const searchAggsSetupMock = (): AggsSetup => ({ export const searchAggsStartMock = (): AggsStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), - getDateMetaByDatatableColumn: jest.fn(), datatableUtilities: { isFilterable: jest.fn(), getAggConfig: jest.fn(), diff --git a/src/plugins/data/public/search/expressions/esdsl.test.ts b/src/plugins/data/public/search/expressions/esdsl.test.ts index edaaa0cae2b7a..197957442cf30 100644 --- a/src/plugins/data/public/search/expressions/esdsl.test.ts +++ b/src/plugins/data/public/search/expressions/esdsl.test.ts @@ -6,7 +6,12 @@ * Side Public License, v 1. */ -import { esdsl } from './esdsl'; +import { getEsdsl } from './esdsl'; +import { MockedKeys } from '@kbn/utility-types/target/jest'; +import { EsdslExpressionFunctionDefinition } from '../../../common/search/expressions'; +import { StartServicesAccessor } from 'kibana/public'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; +import { of } from 'rxjs'; jest.mock('@kbn/i18n', () => { return { @@ -16,26 +21,38 @@ jest.mock('@kbn/i18n', () => { }; }); -jest.mock('../../services', () => ({ - getUiSettings: () => ({ - get: () => true, - }), - getSearchService: () => ({ - search: jest.fn((params: any) => { - return { - toPromise: async () => { - return { rawResponse: params }; +describe('esdsl', () => { + let getStartServices: StartServicesAccessor; + let startDependencies: MockedKeys< + StartServicesAccessor + >; + let esdsl: EsdslExpressionFunctionDefinition; + + beforeEach(() => { + jest.clearAllMocks(); + startDependencies = [ + { + uiSettings: { + get: jest.fn().mockReturnValue(true), }, - }; - }), - }), -})); + }, + {}, + { + search: { + search: jest.fn((params: any) => of({ rawResponse: params })), + }, + }, + ]; + getStartServices = jest + .fn() + .mockResolvedValue(new Promise((resolve) => resolve(startDependencies))); + esdsl = getEsdsl({ getStartServices }); + }); -describe('esdsl', () => { describe('correctly handles input', () => { test('throws on invalid json input', async () => { const fn = async function () { - await esdsl().fn(null, { dsl: 'invalid json', index: 'test', size: 0 }, { + await esdsl.fn(null, { dsl: 'invalid json', index: 'test', size: 0 }, { inspectorAdapters: {}, } as any); }; @@ -50,7 +67,7 @@ describe('esdsl', () => { }); test('adds filters', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', filters: [ @@ -68,7 +85,7 @@ describe('esdsl', () => { }); test('adds filters to query with filters', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', filters: [ @@ -90,7 +107,7 @@ describe('esdsl', () => { }); test('adds query', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', query: { language: 'lucene', query: '*' }, @@ -103,7 +120,7 @@ describe('esdsl', () => { }); test('adds query to a query with filters', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', query: { language: 'lucene', query: '*' }, @@ -120,7 +137,7 @@ describe('esdsl', () => { }); test('ignores timerange', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', timeRange: { from: 'now-15m', to: 'now' }, @@ -134,7 +151,7 @@ describe('esdsl', () => { }); test('correctly handles filter, query and timerange on context', async () => { - const result = await esdsl().fn( + const result = await esdsl.fn( { type: 'kibana_context', query: { language: 'lucene', query: '*' }, diff --git a/src/plugins/data/public/search/expressions/esdsl.ts b/src/plugins/data/public/search/expressions/esdsl.ts index 290f488ef29b5..1dda44ee8993e 100644 --- a/src/plugins/data/public/search/expressions/esdsl.ts +++ b/src/plugins/data/public/search/expressions/esdsl.ts @@ -6,182 +6,37 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; - -import { getSearchService, getUiSettings } from '../../services'; -import { EsRawResponse } from './es_raw_response'; -import { RequestStatistics, RequestAdapter } from '../../../../inspector/common'; -import { IEsSearchResponse, KibanaContext } from '../../../common/search'; -import { buildEsQuery, getEsQueryConfig } from '../../../common/es_query/es_query'; -import { DataPublicPluginStart } from '../../types'; - -const name = 'esdsl'; - -type Input = KibanaContext | null; -type Output = Promise; - -interface Arguments { - dsl: string; - index: string; - size: number; -} - -export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition< - typeof name, - Input, - Arguments, - Output ->; - -export const esdsl = (): EsdslExpressionFunctionDefinition => ({ - name, - type: 'es_raw_response', - inputTypes: ['kibana_context', 'null'], - help: i18n.translate('data.search.esdsl.help', { - defaultMessage: 'Run Elasticsearch request', - }), - args: { - dsl: { - types: ['string'], - aliases: ['_', 'q', 'query'], - help: i18n.translate('data.search.esdsl.q.help', { - defaultMessage: 'Query DSL', - }), - required: true, - }, - index: { - types: ['string'], - help: i18n.translate('data.search.esdsl.index.help', { - defaultMessage: 'ElasticSearch index to query', - }), - required: true, - }, - size: { - types: ['number'], - help: i18n.translate('data.search.esdsl.size.help', { - defaultMessage: 'ElasticSearch searchAPI size parameter', - }), - default: 10, - }, - }, - async fn(input, args, { inspectorAdapters, abortSignal }) { - const searchService: DataPublicPluginStart['search'] = getSearchService(); - - const dsl = JSON.parse(args.dsl); - - if (input) { - const esQueryConfigs = getEsQueryConfig(getUiSettings()); - const query = buildEsQuery( - undefined, // args.index, - input.query || [], - input.filters || [], - esQueryConfigs - ); - - if (!dsl.query) { - dsl.query = query; - } else { - query.bool.must.push(dsl.query); - dsl.query = query; - } - } - - if (!inspectorAdapters.requests) { - inspectorAdapters.requests = new RequestAdapter(); - } - - const request = inspectorAdapters.requests.start( - i18n.translate('data.search.dataRequest.title', { - defaultMessage: 'Data', - }), - { - description: i18n.translate('data.search.es_search.dataRequest.description', { - defaultMessage: - 'This request queries Elasticsearch to fetch the data for the visualization.', - }), - } - ); - - request.stats({ - indexPattern: { - label: i18n.translate('data.search.es_search.indexPatternLabel', { - defaultMessage: 'Index pattern', - }), - value: args.index, - description: i18n.translate('data.search.es_search.indexPatternDescription', { - defaultMessage: 'The index pattern that connected to the Elasticsearch indices.', - }), - }, - }); - - let res: IEsSearchResponse; - try { - res = await searchService - .search( - { - params: { - index: args.index, - size: args.size, - body: dsl, - }, - }, - { abortSignal } - ) - .toPromise(); - - const stats: RequestStatistics = {}; - const resp = res.rawResponse; - - if (resp && resp.took) { - stats.queryTime = { - label: i18n.translate('data.search.es_search.queryTimeLabel', { - defaultMessage: 'Query time', - }), - value: i18n.translate('data.search.es_search.queryTimeValue', { - defaultMessage: '{queryTime}ms', - values: { queryTime: resp.took }, - }), - description: i18n.translate('data.search.es_search.queryTimeDescription', { - defaultMessage: - 'The time it took to process the query. ' + - 'Does not include the time to send the request or parse it in the browser.', - }), - }; - } - - if (resp && resp.hits) { - stats.hitsTotal = { - label: i18n.translate('data.search.es_search.hitsTotalLabel', { - defaultMessage: 'Hits (total)', - }), - value: `${resp.hits.total}`, - description: i18n.translate('data.search.es_search.hitsTotalDescription', { - defaultMessage: 'The number of documents that match the query.', - }), - }; - - stats.hits = { - label: i18n.translate('data.search.es_search.hitsLabel', { - defaultMessage: 'Hits', - }), - value: `${resp.hits.hits.length}`, - description: i18n.translate('data.search.es_search.hitsDescription', { - defaultMessage: 'The number of documents returned by the query.', - }), - }; - } - - request.stats(stats).ok({ json: resp }); - request.json(dsl); - +import { StartServicesAccessor } from 'src/core/public'; +import { DataPublicPluginStart, DataStartDependencies } from '../../types'; +import { getEsdslFn } from '../../../common/search/expressions/esdsl'; +import { UiSettingsCommon } from '../../../common/index_patterns'; + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getEsdsl({ + getStartServices, +}: { + getStartServices: StartServicesAccessor; +}) { + return getEsdslFn({ + getStartDependencies: async () => { + const [core, , { search }] = await getStartServices(); return { - type: 'es_raw_response', - body: resp, + uiSettingsClient: (core.uiSettings as any) as UiSettingsCommon, + search: search.search, }; - } catch (e) { - request.error({ json: e }); - throw e; - } - }, -}); + }, + }); +} diff --git a/src/plugins/data/public/search/expressions/index.ts b/src/plugins/data/public/search/expressions/index.ts index cb4ca4b432610..d60ab610d27b5 100644 --- a/src/plugins/data/public/search/expressions/index.ts +++ b/src/plugins/data/public/search/expressions/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export * from './es_raw_response'; export * from './esaggs'; export * from './esdsl'; +export * from '../../../common/search/expressions'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index a3acd775ee892..83a44b6f68af6 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -33,6 +33,7 @@ import { rangeFilterFunction, kibanaFilterFunction, phraseFilterFunction, + esRawResponse, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -40,7 +41,7 @@ import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -import { esdsl, esRawResponse, getEsaggs } from './expressions'; +import { getEsaggs, getEsdsl } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session'; import { ConfigSchema } from '../../config'; @@ -126,7 +127,11 @@ export class SearchService implements Plugin { expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); - expressions.registerFunction(esdsl); + expressions.registerFunction( + getEsdsl({ getStartServices } as { + getStartServices: StartServicesAccessor; + }) + ); expressions.registerType(esRawResponse); const aggs = this.aggsService.setup({ diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 5d703021b94da..c4cc2073ef78f 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -13,9 +13,11 @@ import { Logger, SavedObjectsClientContract, ElasticsearchClient, + UiSettingsServiceStart, } from 'kibana/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; -import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { DataPluginStart } from '../plugin'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; @@ -25,6 +27,7 @@ import { getIndexPatternLoad } from './expressions'; import { UiSettingsServerToCommon } from './ui_settings_wrapper'; import { IndexPatternsApiServer } from './index_patterns_api_client'; import { SavedObjectsClientServerToCommon } from './saved_objects_client_wrapper'; +import { registerIndexPatternsUsageCollector } from './register_index_pattern_usage_collection'; export interface IndexPatternsServiceStart { indexPatternsServiceFactory: ( @@ -35,6 +38,8 @@ export interface IndexPatternsServiceStart { export interface IndexPatternsServiceSetupDeps { expressions: ExpressionsServerSetup; + logger: Logger; + usageCollection?: UsageCollectionSetup; } export interface IndexPatternsServiceStartDeps { @@ -42,10 +47,39 @@ export interface IndexPatternsServiceStartDeps { logger: Logger; } +export const indexPatternsServiceFactory = ({ + logger, + uiSettings, + fieldFormats, +}: { + logger: Logger; + uiSettings: UiSettingsServiceStart; + fieldFormats: FieldFormatsStart; +}) => async ( + savedObjectsClient: SavedObjectsClientContract, + elasticsearchClient: ElasticsearchClient +) => { + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); + + return new IndexPatternsCommonService({ + uiSettings: new UiSettingsServerToCommon(uiSettingsClient), + savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), + apiClient: new IndexPatternsApiServer(elasticsearchClient), + fieldFormats: formats, + onError: (error) => { + logger.error(error); + }, + onNotification: ({ title, text }) => { + logger.warn(`${title} : ${text}`); + }, + }); +}; + export class IndexPatternsServiceProvider implements Plugin { public setup( - core: CoreSetup, - { expressions }: IndexPatternsServiceSetupDeps + core: CoreSetup, + { expressions, usageCollection }: IndexPatternsServiceSetupDeps ) { core.savedObjects.registerType(indexPatternSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); @@ -53,32 +87,18 @@ export class IndexPatternsServiceProvider implements Plugin { - const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); - const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); - - return new IndexPatternsCommonService({ - uiSettings: new UiSettingsServerToCommon(uiSettingsClient), - savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), - apiClient: new IndexPatternsApiServer(elasticsearchClient), - fieldFormats: formats, - onError: (error) => { - logger.error(error); - }, - onNotification: ({ title, text }) => { - logger.warn(`${title} : ${text}`); - }, - }); - }, + indexPatternsServiceFactory: indexPatternsServiceFactory({ + logger, + uiSettings, + fieldFormats, + }), }; } } diff --git a/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.test.ts b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.test.ts new file mode 100644 index 0000000000000..c43431e10731a --- /dev/null +++ b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + minMaxAvgLoC, + updateMin, + updateMax, + getIndexPatternTelemetry, +} from './register_index_pattern_usage_collection'; +import { IndexPatternsCommonService } from '..'; + +const scriptA = 'emit(0);'; +const scriptB = 'emit(1);\nemit(2);'; +const scriptC = 'emit(3);\nemit(4)\nemit(5)'; + +const scriptedFieldA = { script: scriptA }; +const scriptedFieldB = { script: scriptB }; +const scriptedFieldC = { script: scriptC }; + +const runtimeFieldA = { runtimeField: { script: { source: scriptA } } }; +const runtimeFieldB = { runtimeField: { script: { source: scriptB } } }; +const runtimeFieldC = { runtimeField: { script: { source: scriptC } } }; + +const indexPatterns = ({ + getIds: async () => [1, 2, 3], + get: jest.fn().mockResolvedValue({ + getScriptedFields: () => [], + fields: [], + }), +} as any) as IndexPatternsCommonService; + +describe('index pattern usage collection', () => { + it('minMaxAvgLoC calculates min, max, and average ', () => { + const scripts = [scriptA, scriptB, scriptC]; + expect(minMaxAvgLoC(scripts)).toEqual({ min: 1, max: 3, avg: 2 }); + expect(minMaxAvgLoC([undefined, undefined, undefined])).toEqual({ min: 0, max: 0, avg: 0 }); + }); + + it('updateMin returns minimum value', () => { + expect(updateMin(undefined, 1)).toEqual(1); + expect(updateMin(1, 0)).toEqual(0); + }); + + it('updateMax returns maximum value', () => { + expect(updateMax(undefined, 1)).toEqual(1); + expect(updateMax(1, 0)).toEqual(1); + }); + + describe('calculates index pattern usage', () => { + const countSummaryDefault = { + min: undefined, + max: undefined, + avg: undefined, + }; + + it('when there are no runtime fields or scripted fields', async () => { + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 0, + indexPatternsWithRuntimeFieldCount: 0, + scriptedFieldCount: 0, + runtimeFieldCount: 0, + perIndexPattern: { + scriptedFieldCount: countSummaryDefault, + runtimeFieldCount: countSummaryDefault, + scriptedFieldLineCount: countSummaryDefault, + runtimeFieldLineCount: countSummaryDefault, + }, + }); + }); + + it('when there are both runtime fields or scripted fields', async () => { + indexPatterns.get = jest.fn().mockResolvedValue({ + getScriptedFields: () => [scriptedFieldA, scriptedFieldB, scriptedFieldC], + fields: [runtimeFieldA, runtimeFieldB, runtimeFieldC], + }); + + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 3, + indexPatternsWithRuntimeFieldCount: 3, + scriptedFieldCount: 9, + runtimeFieldCount: 9, + perIndexPattern: { + scriptedFieldCount: { min: 3, max: 3, avg: 3 }, + runtimeFieldCount: { min: 3, max: 3, avg: 3 }, + scriptedFieldLineCount: { min: 1, max: 3, avg: 2 }, + runtimeFieldLineCount: { min: 1, max: 3, avg: 2 }, + }, + }); + }); + + it('when there are only runtime fields', async () => { + indexPatterns.get = jest.fn().mockResolvedValue({ + getScriptedFields: () => [], + fields: [runtimeFieldA, runtimeFieldB, runtimeFieldC], + }); + + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 0, + indexPatternsWithRuntimeFieldCount: 3, + scriptedFieldCount: 0, + runtimeFieldCount: 9, + perIndexPattern: { + scriptedFieldCount: countSummaryDefault, + runtimeFieldCount: { min: 3, max: 3, avg: 3 }, + scriptedFieldLineCount: countSummaryDefault, + runtimeFieldLineCount: { min: 1, max: 3, avg: 2 }, + }, + }); + }); + + it('when there are only scripted fields', async () => { + indexPatterns.get = jest.fn().mockResolvedValue({ + getScriptedFields: () => [scriptedFieldA, scriptedFieldB, scriptedFieldC], + fields: [], + }); + + expect(await getIndexPatternTelemetry(indexPatterns)).toEqual({ + indexPatternsCount: 3, + indexPatternsWithScriptedFieldCount: 3, + indexPatternsWithRuntimeFieldCount: 0, + scriptedFieldCount: 9, + runtimeFieldCount: 0, + perIndexPattern: { + scriptedFieldCount: { min: 3, max: 3, avg: 3 }, + runtimeFieldCount: countSummaryDefault, + scriptedFieldLineCount: { min: 1, max: 3, avg: 2 }, + runtimeFieldLineCount: countSummaryDefault, + }, + }); + }); + }); +}); diff --git a/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.ts b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.ts new file mode 100644 index 0000000000000..36c2a59ce2753 --- /dev/null +++ b/src/plugins/data/server/index_patterns/register_index_pattern_usage_collection.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { StartServicesAccessor } from 'src/core/server'; +import { IndexPatternsCommonService } from '..'; +import { SavedObjectsClient } from '../../../../core/server'; +import { DataPluginStartDependencies, DataPluginStart } from '../plugin'; + +interface CountSummary { + min?: number; + max?: number; + avg?: number; +} + +interface IndexPatternUsage { + indexPatternsCount: number; + indexPatternsWithScriptedFieldCount: number; + indexPatternsWithRuntimeFieldCount: number; + scriptedFieldCount: number; + runtimeFieldCount: number; + perIndexPattern: { + scriptedFieldCount: CountSummary; + runtimeFieldCount: CountSummary; + scriptedFieldLineCount: CountSummary; + runtimeFieldLineCount: CountSummary; + }; +} + +export const minMaxAvgLoC = (scripts: Array) => { + const lengths = scripts.map((script) => script?.split(/\r\n|\r|\n/).length || 0).sort(); + return { + min: lengths[0], + max: lengths[lengths.length - 1], + avg: lengths.reduce((col, count) => col + count, 0) / lengths.length, + }; +}; + +export const updateMin = (currentMin: number | undefined, newVal: number): number => { + if (currentMin === undefined || currentMin > newVal) { + return newVal; + } else { + return currentMin; + } +}; + +export const updateMax = (currentMax: number | undefined, newVal: number): number => { + if (currentMax === undefined || currentMax < newVal) { + return newVal; + } else { + return currentMax; + } +}; + +export async function getIndexPatternTelemetry(indexPatterns: IndexPatternsCommonService) { + const ids = await indexPatterns.getIds(); + + const countSummaryDefaults: CountSummary = { + min: undefined, + max: undefined, + avg: undefined, + }; + + const results = { + indexPatternsCount: ids.length, + indexPatternsWithScriptedFieldCount: 0, + indexPatternsWithRuntimeFieldCount: 0, + scriptedFieldCount: 0, + runtimeFieldCount: 0, + perIndexPattern: { + scriptedFieldCount: { ...countSummaryDefaults }, + runtimeFieldCount: { ...countSummaryDefaults }, + scriptedFieldLineCount: { ...countSummaryDefaults }, + runtimeFieldLineCount: { ...countSummaryDefaults }, + }, + }; + + await ids.reduce(async (col, id) => { + await col; + const ip = await indexPatterns.get(id); + + const scriptedFields = ip.getScriptedFields(); + const runtimeFields = ip.fields.filter((fld) => !!fld.runtimeField); + + if (scriptedFields.length > 0) { + // increment counts + results.indexPatternsWithScriptedFieldCount++; + results.scriptedFieldCount += scriptedFields.length; + + // calc LoC + results.perIndexPattern.scriptedFieldLineCount = minMaxAvgLoC( + scriptedFields.map((fld) => fld.script || '') + ); + + // calc field counts + results.perIndexPattern.scriptedFieldCount.min = updateMin( + results.perIndexPattern.scriptedFieldCount.min, + scriptedFields.length + ); + results.perIndexPattern.scriptedFieldCount.max = updateMax( + results.perIndexPattern.scriptedFieldCount.max, + scriptedFields.length + ); + results.perIndexPattern.scriptedFieldCount.avg = + results.scriptedFieldCount / results.indexPatternsWithScriptedFieldCount; + } + + if (runtimeFields.length > 0) { + // increment counts + results.indexPatternsWithRuntimeFieldCount++; + results.runtimeFieldCount += runtimeFields.length; + + // calc LoC + const runtimeFieldScripts = runtimeFields.map( + (fld) => fld.runtimeField?.script?.source || '' + ); + results.perIndexPattern.runtimeFieldLineCount = minMaxAvgLoC(runtimeFieldScripts); + + // calc field counts + results.perIndexPattern.runtimeFieldCount.min = updateMin( + results.perIndexPattern.runtimeFieldCount.min, + runtimeFields.length + ); + results.perIndexPattern.runtimeFieldCount.max = updateMax( + results.perIndexPattern.runtimeFieldCount.max, + runtimeFields.length + ); + results.perIndexPattern.runtimeFieldCount.avg = + results.runtimeFieldCount / results.indexPatternsWithRuntimeFieldCount; + } + }, Promise.resolve()); + + return results; +} + +export function registerIndexPatternsUsageCollector( + getStartServices: StartServicesAccessor, + usageCollection?: UsageCollectionSetup +): void { + if (!usageCollection) { + return; + } + + const indexPatternUsageCollector = usageCollection.makeUsageCollector({ + type: 'index-patterns', + isReady: () => true, + fetch: async () => { + const [{ savedObjects, elasticsearch }, , { indexPatterns }] = await getStartServices(); + const indexPatternService = await indexPatterns.indexPatternsServiceFactory( + new SavedObjectsClient(savedObjects.createInternalRepository()), + elasticsearch.client.asInternalUser + ); + + return await getIndexPatternTelemetry(indexPatternService); + }, + schema: { + indexPatternsCount: { type: 'long' }, + indexPatternsWithScriptedFieldCount: { type: 'long' }, + indexPatternsWithRuntimeFieldCount: { type: 'long' }, + scriptedFieldCount: { type: 'long' }, + runtimeFieldCount: { type: 'long' }, + perIndexPattern: { + scriptedFieldCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + runtimeFieldCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + scriptedFieldLineCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + runtimeFieldLineCount: { + min: { type: 'long' }, + max: { type: 'long' }, + avg: { type: 'float' }, + }, + }, + }, + }); + + usageCollection.registerCollector(indexPatternUsageCollector); +} diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index a7a7663d6981c..7b73802f1a34d 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -46,8 +46,10 @@ export interface DataPluginSetupDependencies { usageCollection?: UsageCollectionSetup; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataPluginStartDependencies {} +export interface DataPluginStartDependencies { + fieldFormats: FieldFormatsStart; + logger: Logger; +} export class DataServerPlugin implements @@ -82,7 +84,11 @@ export class DataServerPlugin this.queryService.setup(core); this.autocompleteService.setup(core); this.kqlTelemetryService.setup(core, { usageCollection }); - this.indexPatterns.setup(core, { expressions }); + this.indexPatterns.setup(core, { + expressions, + logger: this.logger.get('indexPatterns'), + usageCollection, + }); core.uiSettings.register(getUiSettings()); diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index ac81c5b0d5df4..96927728f2f2f 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -74,7 +74,6 @@ export class AggsService { const { calculateAutoTimeExpression, - getDateMetaByDatatableColumn, datatableUtilities, types, } = this.aggsCommonService.start({ @@ -119,7 +118,6 @@ export class AggsService { return { calculateAutoTimeExpression, - getDateMetaByDatatableColumn, datatableUtilities, createAggConfigs: (indexPattern, configStates = []) => { return new AggConfigs(indexPattern, configStates, { typesRegistry }); diff --git a/src/plugins/data/server/search/aggs/mocks.ts b/src/plugins/data/server/search/aggs/mocks.ts index 70864d57b3565..3644a3c13c48d 100644 --- a/src/plugins/data/server/search/aggs/mocks.ts +++ b/src/plugins/data/server/search/aggs/mocks.ts @@ -58,7 +58,6 @@ export const searchAggsSetupMock = (): AggsSetup => ({ const commonStartMock = (): AggsCommonStart => ({ calculateAutoTimeExpression: getCalculateAutoTimeExpression(getConfig), - getDateMetaByDatatableColumn: jest.fn(), datatableUtilities: { getIndexPattern: jest.fn(), getAggConfig: jest.fn(), diff --git a/src/plugins/data/server/search/expressions/esdsl.ts b/src/plugins/data/server/search/expressions/esdsl.ts new file mode 100644 index 0000000000000..e16204c8782e4 --- /dev/null +++ b/src/plugins/data/server/search/expressions/esdsl.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { StartServicesAccessor } from 'src/core/server'; +import { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { getEsdslFn } from '../../../common/search/expressions/esdsl'; + +/** + * This is some glue code that takes in `core.getStartServices`, extracts the dependencies + * needed for this function, and wraps them behind a `getStartDependencies` function that + * is then called at runtime. + * + * We do this so that we can be explicit about exactly which dependencies the function + * requires, without cluttering up the top-level `plugin.ts` with this logic. It also + * makes testing the expression function a bit easier since `getStartDependencies` is + * the only thing you should need to mock. + * + * @param getStartServices - core's StartServicesAccessor for this plugin + * + * @internal + */ +export function getEsdsl({ + getStartServices, +}: { + getStartServices: StartServicesAccessor; +}) { + return getEsdslFn({ + getStartDependencies: async (getKibanaRequest: any) => { + const [core, , { search }] = await getStartServices(); + if (!getKibanaRequest || !getKibanaRequest()) { + throw new Error('TODO: add text'); + } + const request = getKibanaRequest(); + const savedObjectsClient = core.savedObjects.getScopedClient(request); + return { + uiSettingsClient: core.uiSettings.asScopedToClient(savedObjectsClient), + search: search.asScoped(request).search, + }; + }, + }); +} diff --git a/src/plugins/data/server/search/expressions/index.ts b/src/plugins/data/server/search/expressions/index.ts index a052066186235..bb690c2e6e7c6 100644 --- a/src/plugins/data/server/search/expressions/index.ts +++ b/src/plugins/data/server/search/expressions/index.ts @@ -7,3 +7,4 @@ */ export * from './esaggs'; +export * from './esdsl'; diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 192c133c94a04..d5a83efcc215f 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -10,7 +10,7 @@ import type { MockedKeys } from '@kbn/utility-types/jest'; import { CoreSetup, CoreStart, SavedObject } from '../../../../core/server'; import { coreMock } from '../../../../core/server/mocks'; -import { DataPluginStart } from '../plugin'; +import { DataPluginStart, DataPluginStartDependencies } from '../plugin'; import { createFieldFormatsStartMock } from '../field_formats/mocks'; import { createIndexPatternsStartMock } from '../index_patterns/mocks'; @@ -32,7 +32,7 @@ import { createSearchSessionsClientMock } from './mocks'; describe('Search service', () => { let plugin: SearchService; - let mockCoreSetup: MockedKeys>; + let mockCoreSetup: MockedKeys>; let mockCoreStart: MockedKeys; beforeEach(() => { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index fdf0b66197b34..e53244fa7ff26 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -38,7 +38,7 @@ import { FieldFormatsStart } from '../field_formats'; import { IndexPatternsServiceStart } from '../index_patterns'; import { getCallMsearch, registerMsearchRoute, registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; -import { DataPluginStart } from '../plugin'; +import { DataPluginStart, DataPluginStartDependencies } from '../plugin'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { registerUsageCollector } from './collectors/register'; import { usageProvider } from './collectors/usage'; @@ -63,8 +63,9 @@ import { searchSourceRequiredUiSettings, SearchSourceService, phraseFilterFunction, + esRawResponse, } from '../../common/search'; -import { getEsaggs } from './expressions'; +import { getEsaggs, getEsdsl } from './expressions'; import { getShardDelayBucketAgg, SHARD_DELAY_AGG_NAME, @@ -113,7 +114,7 @@ export class SearchService implements Plugin { } public setup( - core: CoreSetup<{}, DataPluginStart>, + core: CoreSetup, { bfetch, expressions, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { const usage = usageCollection ? usageProvider(core) : undefined; @@ -150,6 +151,7 @@ export class SearchService implements Plugin { } expressions.registerFunction(getEsaggs({ getStartServices: core.getStartServices })); + expressions.registerFunction(getEsdsl({ getStartServices: core.getStartServices })); expressions.registerFunction(kibana); expressions.registerFunction(luceneFunction); expressions.registerFunction(kqlFunction); @@ -162,6 +164,7 @@ export class SearchService implements Plugin { expressions.registerFunction(rangeFilterFunction); expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); + expressions.registerType(esRawResponse); const aggs = this.aggsService.setup({ registerFunction: expressions.registerFunction }); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 12458d7a74d9f..29a5a67239171 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -73,6 +73,7 @@ import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiCounterMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; +import { UsageCollectionSetup as UsageCollectionSetup_2 } from 'src/plugins/usage_collection/server'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -202,6 +203,10 @@ export interface AggFunctionsMapping { // // (undocumented) aggSignificantTerms: ReturnType; + // Warning: (ae-forgotten-export) The symbol "aggSinglePercentile" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggSinglePercentile: ReturnType; // Warning: (ae-forgotten-export) The symbol "aggStdDeviation" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -403,7 +408,7 @@ export const ES_SEARCH_STRATEGY = "es"; // Warning: (ae-missing-release-tag) "EsaggsExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>; +export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_36, Arguments_21, Output_36>; // Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -954,16 +959,14 @@ export { IndexPatternsService } // // @public (undocumented) export class IndexPatternsServiceProvider implements Plugin_3 { - // Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceSetupDeps" needs to be exported by the entry point index.d.ts // // (undocumented) - setup(core: CoreSetup_2, { expressions }: IndexPatternsServiceSetupDeps): void; - // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts - // + setup(core: CoreSetup_2, { expressions, usageCollection }: IndexPatternsServiceSetupDeps): void; // (undocumented) start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { - indexPatternsServiceFactory: (savedObjectsClient: SavedObjectsClientContract_2, elasticsearchClient: ElasticsearchClient_2) => Promise; + indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: ElasticsearchClient_2) => Promise; }; } @@ -1163,6 +1166,8 @@ export enum METRIC_TYPES { // (undocumented) SERIAL_DIFF = "serial_diff", // (undocumented) + SINGLE_PERCENTILE = "single_percentile", + // (undocumented) STD_DEV = "std_dev", // (undocumented) SUM = "sum", @@ -1207,6 +1212,7 @@ export type ParsedInterval = ReturnType; export function parseInterval(interval: string): moment.Duration | null; // Warning: (ae-forgotten-export) The symbol "DataPluginSetupDependencies" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DataPluginStartDependencies" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DataServerPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1297,7 +1303,7 @@ export const search: { intervalOptions: ({ display: string; val: string; - enabled(agg: import("../common").IBucketAggConfig): boolean | "" | undefined; + enabled(agg: import("../common").IBucketAggConfig): boolean; } | { display: string; val: string; @@ -1516,7 +1522,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:267:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:270:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:271:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/plugin.ts:81:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts index 29c93886ebba3..5d2be533065e7 100644 --- a/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts +++ b/src/plugins/discover/public/application/components/histogram/apply_aggs_to_search_source.test.ts @@ -24,45 +24,46 @@ describe('applyAggsToSearchSource', () => { const aggsConfig = applyAggsToSearchSource(true, searchSource, 'auto', indexPattern, dataMock); expect(aggsConfig!.aggs).toMatchInlineSnapshot(` - Array [ - Object { - "enabled": true, - "id": "1", - "params": Object {}, - "schema": "metric", - "type": "count", - }, - Object { - "enabled": true, - "id": "2", - "params": Object { - "drop_partials": false, - "extended_bounds": Object {}, - "field": "timestamp", - "interval": "auto", - "min_doc_count": 1, - "scaleMetricValues": false, - "useNormalizedEsInterval": true, + Array [ + Object { + "enabled": true, + "id": "1", + "params": Object {}, + "schema": "metric", + "type": "count", }, - "schema": "segment", - "type": "date_histogram", - }, - ] - `); + Object { + "enabled": true, + "id": "2", + "params": Object { + "drop_partials": false, + "extended_bounds": Object {}, + "field": "timestamp", + "interval": "auto", + "min_doc_count": 1, + "scaleMetricValues": false, + "useNormalizedEsInterval": true, + "used_interval": "0ms", + }, + "schema": "segment", + "type": "date_histogram", + }, + ] + `); expect(setField).toHaveBeenCalledWith('aggs', expect.any(Function)); const dslFn = setField.mock.calls[0][1]; expect(dslFn()).toMatchInlineSnapshot(` - Object { - "2": Object { - "date_histogram": Object { - "field": "timestamp", - "min_doc_count": 1, - "time_zone": "America/New_York", - }, - }, - } - `); + Object { + "2": Object { + "date_histogram": Object { + "field": "timestamp", + "min_doc_count": 1, + "time_zone": "America/New_York", + }, + }, + } + `); }); test('enabled = false', () => { diff --git a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts index a75d6f6c06a95..ed9e588a999b4 100644 --- a/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts +++ b/src/plugins/home/server/tutorials/instructions/cloud_instructions.ts @@ -7,15 +7,14 @@ */ import { i18n } from '@kbn/i18n'; - export const cloudPasswordAndResetLink = i18n.translate( 'home.tutorials.common.cloudInstructions.passwordAndResetLink', { defaultMessage: 'Where {passwordTemplate} is the password of the `elastic` user.' + - `\\{#config.cloud.resetPasswordUrl\\} - Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.resetPasswordUrl\\}). - \\{/config.cloud.resetPasswordUrl\\}`, + `\\{#config.cloud.profileUrl\\} + Forgot the password? [Reset in Elastic Cloud](\\{config.cloud.baseUrl\\}\\{config.cloud.profileUrl\\}). + \\{/config.cloud.profileUrl\\}`, values: { passwordTemplate: '``' }, } ); diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index fa0a32fc3d542..0054bb9c01b41 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -347,20 +347,6 @@ class TableListView extends React.Component - ); - } - } - renderToolsLeft() { const selection = this.state.selectedIds; @@ -473,10 +459,9 @@ class TableListView extends React.Component = { type: 'keyword', _meta: { description: 'Default value of the setting was changed.' }, }, + 'banners:textContent': { + type: 'keyword', + _meta: { description: 'Default value of the setting was changed.' }, + }, // non-sensitive 'visualize:enableLabs': { type: 'boolean', @@ -408,6 +412,18 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'banners:placement': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, + 'banners:textColor': { + type: 'text', + _meta: { description: 'Non-default value of setting.' }, + }, + 'banners:backgroundColor': { + type: 'text', + _meta: { description: 'Non-default value of setting.' }, + }, 'observability:enableAlertingExperience': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index c4a70f5065d8e..810f13931225f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -18,6 +18,7 @@ export interface UsageStats { 'timelion:graphite.url': string; 'xpackDashboardMode:roles': string; 'securitySolution:ipReputationLinks': string; + 'banners:textContent': string; /** * non-sensitive settings */ @@ -114,4 +115,7 @@ export interface UsageStats { 'csv:quoteValues': boolean; 'dateFormat:dow': string; dateFormat: string; + 'banners:placement': string; + 'banners:textColor': string; + 'banners:backgroundColor': string; } diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx index 57d05262319f2..4491be04b1a42 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard.tsx @@ -28,6 +28,7 @@ interface SaveModalDocumentInfo { export interface SaveModalDashboardProps { documentInfo: SaveModalDocumentInfo; + canSaveByReference: boolean; objectType: string; onClose: () => void; onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; @@ -35,7 +36,7 @@ export interface SaveModalDashboardProps { } export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { - const { documentInfo, tagOptions, objectType, onClose } = props; + const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; const { id: documentId } = documentInfo; const initialCopyOnSave = !Boolean(documentId); @@ -49,7 +50,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { documentId || disableDashboardOptions ? null : 'existing' ); const [isAddToLibrarySelected, setAddToLibrary] = useState( - !initialCopyOnSave || disableDashboardOptions + canSaveByReference && (!initialCopyOnSave || disableDashboardOptions) ); const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( null @@ -65,13 +66,16 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { onChange={(option) => { setDashboardOption(option); }} + canSaveByReference={canSaveByReference} {...{ copyOnSave, documentId, dashboardOption, setAddToLibrary, isAddToLibrarySelected }} /> ) : null; const onCopyOnSaveChange = (newCopyOnSave: boolean) => { - setAddToLibrary(true); + if (canSaveByReference) { + setAddToLibrary(true); + } setDashboardOption(null); setCopyOnSave(newCopyOnSave); }; diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx index dd6fd975f8e07..341f194b71ba4 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.stories.tsx @@ -33,15 +33,21 @@ export default { control: 'boolean', defaultValue: true, }, + canSaveVisualizations: { + control: 'boolean', + defaultValue: true, + }, }, }; export function Example({ copyOnSave, hasDocumentId, + canSaveVisualizations, }: { copyOnSave: boolean; hasDocumentId: boolean; + canSaveVisualizations: boolean; } & StorybookParams) { const [dashboardOption, setDashboardOption] = useState<'new' | 'existing' | null>('existing'); const [isAddToLibrarySelected, setAddToLibrary] = useState(false); @@ -52,6 +58,7 @@ export function Example({ onChange={setDashboardOption} dashboardOption={dashboardOption} copyOnSave={copyOnSave} + canSaveByReference={canSaveVisualizations} documentId={hasDocumentId ? 'abc' : undefined} isAddToLibrarySelected={isAddToLibrarySelected} setAddToLibrary={setAddToLibrary} diff --git a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx index 1ae54040571a2..78a1569c02ead 100644 --- a/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx +++ b/src/plugins/presentation_util/public/components/saved_object_save_modal_dashboard_selector.tsx @@ -30,6 +30,7 @@ export interface SaveModalDashboardSelectorProps { copyOnSave: boolean; documentId?: string; onSelectDashboard: DashboardPickerProps['onChange']; + canSaveByReference: boolean; setAddToLibrary: (selected: boolean) => void; isAddToLibrarySelected: boolean; dashboardOption: 'new' | 'existing' | null; @@ -40,6 +41,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp const { documentId, onSelectDashboard, + canSaveByReference, setAddToLibrary, isAddToLibrarySelected, dashboardOption, @@ -114,7 +116,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp setAddToLibrary(true); onChange(null); }} - disabled={isDisabled} + disabled={isDisabled || !canSaveByReference} />
@@ -127,7 +129,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp defaultMessage: 'Add to library', })} checked={isAddToLibrarySelected} - disabled={dashboardOption === null || isDisabled} + disabled={dashboardOption === null || isDisabled || !canSaveByReference} onChange={(event) => setAddToLibrary(event.target.checked)} /> diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index 74607b9e35e47..39dae92aa2ba9 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -23,6 +23,7 @@ export interface PresentationDashboardsService { export interface PresentationCapabilitiesService { canAccessDashboards: () => boolean; canCreateNewDashboards: () => boolean; + canSaveVisualizations: () => boolean; } export interface PresentationUtilServices { diff --git a/src/plugins/presentation_util/public/services/kibana/capabilities.ts b/src/plugins/presentation_util/public/services/kibana/capabilities.ts index 546281d083f2f..6949fba00c65a 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -16,10 +16,11 @@ export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< >; export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => { - const { dashboard } = coreStart.application.capabilities; + const { dashboard, visualize } = coreStart.application.capabilities; return { canAccessDashboards: () => Boolean(dashboard.show), canCreateNewDashboards: () => Boolean(dashboard.createNew), + canSaveVisualizations: () => Boolean(visualize.save), }; }; diff --git a/src/plugins/presentation_util/public/services/storybook/capabilities.ts b/src/plugins/presentation_util/public/services/storybook/capabilities.ts index fcd38b29f154c..16fbe3baf488f 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -19,11 +19,13 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ canAccessDashboards, canCreateNewDashboards, canEditDashboards, + canSaveVisualizations, }) => { const check = (value: boolean = true) => value; return { canAccessDashboards: () => check(canAccessDashboards), canCreateNewDashboards: () => check(canCreateNewDashboards), canEditDashboards: () => check(canEditDashboards), + canSaveVisualizations: () => check(canSaveVisualizations), }; }; diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index 37b2171635e96..dd7de54264062 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -18,6 +18,7 @@ export interface StorybookParams { canAccessDashboards?: boolean; canCreateNewDashboards?: boolean; canEditDashboards?: boolean; + canSaveVisualizations?: boolean; } export const providers: PluginServiceProviders = { diff --git a/src/plugins/presentation_util/public/services/stub/capabilities.ts b/src/plugins/presentation_util/public/services/stub/capabilities.ts index 979ccc8faadd5..4154fa65a0cd7 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -15,4 +15,5 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({ canAccessDashboards: () => true, canCreateNewDashboards: () => true, canEditDashboards: () => true, + canSaveVisualizations: () => true, }); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index ee96ae041dd09..05ac1eb84089d 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -24,6 +24,81 @@ } } }, + "index-patterns": { + "properties": { + "indexPatternsCount": { + "type": "long" + }, + "indexPatternsWithScriptedFieldCount": { + "type": "long" + }, + "indexPatternsWithRuntimeFieldCount": { + "type": "long" + }, + "scriptedFieldCount": { + "type": "long" + }, + "runtimeFieldCount": { + "type": "long" + }, + "perIndexPattern": { + "properties": { + "scriptedFieldCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + }, + "runtimeFieldCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + }, + "scriptedFieldLineCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + }, + "runtimeFieldLineCount": { + "properties": { + "min": { + "type": "long" + }, + "max": { + "type": "long" + }, + "avg": { + "type": "float" + } + } + } + } + } + } + }, "kql": { "properties": { "optInCount": { @@ -7478,6 +7553,12 @@ "description": "Default value of the setting was changed." } }, + "banners:textContent": { + "type": "keyword", + "_meta": { + "description": "Default value of the setting was changed." + } + }, "visualize:enableLabs": { "type": "boolean", "_meta": { @@ -8027,6 +8108,24 @@ "description": "Non-default value of setting." } }, + "banners:placement": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + }, + "banners:textColor": { + "type": "text", + "_meta": { + "description": "Non-default value of setting." + } + }, + "banners:backgroundColor": { + "type": "text", + "_meta": { + "description": "Non-default value of setting." + } + }, "observability:enableAlertingExperience": { "type": "boolean", "_meta": { diff --git a/src/plugins/vis_type_metric/public/metric_vis_type.ts b/src/plugins/vis_type_metric/public/metric_vis_type.ts index 9e2e248c6ccd5..382ef925c5282 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_type.ts @@ -64,6 +64,7 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition => '!cumulative_sum', '!geo_bounds', '!filtered_metric', + '!single_percentile', ], aggSettings: { top_hits: { diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts index 2f30faa8e9a89..e582f098a5fd5 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts @@ -50,7 +50,7 @@ export const tableVisLegacyTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { defaultMessage: 'Metric', }), - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], aggSettings: { top_hits: { allowStrings: true, diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index d645af3180b08..a49748fe86c96 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -46,7 +46,7 @@ export const tableVisTypeDefinition: VisTypeDefinition = { title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { defaultMessage: 'Metric', }), - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], aggSettings: { top_hits: { allowStrings: true, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 4052ecbe21997..960122c178caa 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -52,6 +52,7 @@ export const tagCloudVisTypeDefinition = { '!geo_bounds', '!geo_centroid', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_timeseries/common/interval_regexp.test.js b/src/plugins/vis_type_timeseries/common/interval_regexp.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/common/interval_regexp.test.js rename to src/plugins/vis_type_timeseries/common/interval_regexp.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts similarity index 82% rename from src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js rename to src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts index 016d119ed1675..a602b34d99986 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.js +++ b/src/plugins/vis_type_timeseries/public/application/lib/validate_interval.ts @@ -6,12 +6,20 @@ * Side Public License, v 1. */ -import { GTE_INTERVAL_RE } from '../../../common/interval_regexp'; import { i18n } from '@kbn/i18n'; +import { GTE_INTERVAL_RE } from '../../../common/interval_regexp'; import { search } from '../../../../../plugins/data/public'; + +import type { TimeRangeBounds } from '../../../../data/common'; +import type { TimeseriesVisParams } from '../../types'; + const { parseInterval } = search.aggs; -export function validateInterval(bounds, panel, maxBuckets) { +export function validateInterval( + bounds: TimeRangeBounds, + panel: TimeseriesVisParams, + maxBuckets: number +) { const { interval } = panel; const { min, max } = bounds; // No need to check auto it will return around 100 @@ -20,8 +28,9 @@ export function validateInterval(bounds, panel, maxBuckets) { const greaterThanMatch = interval.match(GTE_INTERVAL_RE); if (greaterThanMatch) return; const duration = parseInterval(interval); + if (duration) { - const span = max.valueOf() - min.valueOf(); + const span = max!.valueOf() - min!.valueOf(); const buckets = Math.floor(span / duration.asMilliseconds()); if (buckets > maxBuckets) { throw new Error( diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index bf3779674b6ea..bf58287870c82 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -28,14 +28,15 @@ export const metricsRequestHandler = async ({ searchSessionId, }: MetricsRequestHandlerParams): Promise => { const config = getUISettings(); + const data = getDataStart(); + const timezone = getTimezone(config); const uiStateObj = uiState[visParams.type] ?? {}; - const data = getDataStart(); - const dataSearch = getDataStart().search; + const dataSearch = data.search; const parsedTimeRange = data.query.timefilter.timefilter.calculateBounds(input?.timeRange!); if (visParams && visParams.id && !visParams.isModelInvalid) { - const maxBuckets = config.get(MAX_BUCKETS_SETTING); + const maxBuckets = config.get(MAX_BUCKETS_SETTING); validateInterval(parsedTimeRange, visParams, maxBuckets); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts index e2012c5331516..8029e8684c441 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.test.ts @@ -7,20 +7,19 @@ */ import { DefaultSearchCapabilities } from './default_search_capabilities'; -import { VisTypeTimeseriesRequest } from '../../../types'; describe('DefaultSearchCapabilities', () => { let defaultSearchCapabilities: DefaultSearchCapabilities; - let req: VisTypeTimeseriesRequest; beforeEach(() => { - req = {} as VisTypeTimeseriesRequest; - defaultSearchCapabilities = new DefaultSearchCapabilities(req); + defaultSearchCapabilities = new DefaultSearchCapabilities({ + timezone: 'UTC', + maxBucketsLimit: 2000, + }); }); test('should init default search capabilities', () => { - expect(defaultSearchCapabilities.request).toBe(req); - expect(defaultSearchCapabilities.fieldsCapabilities).toEqual({}); + expect(defaultSearchCapabilities.timezone).toBe('UTC'); }); test('should return defaultTimeInterval', () => { @@ -35,18 +34,6 @@ describe('DefaultSearchCapabilities', () => { }); }); - test('should return Search Timezone', () => { - defaultSearchCapabilities.request = ({ - body: { - timerange: { - timezone: 'UTC', - }, - }, - } as unknown) as VisTypeTimeseriesRequest; - - expect(defaultSearchCapabilities.searchTimezone).toEqual('UTC'); - }); - test('should return a valid time interval', () => { expect(defaultSearchCapabilities.getValidTimeInterval('20m')).toBe('20m'); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts index 967b34707de53..b60d2e61e9a43 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/default_search_capabilities.ts @@ -13,23 +13,20 @@ import { getSuitableUnit, } from '../../vis_data/helpers/unit_to_seconds'; import { RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions'; -import { VisTypeTimeseriesRequest, VisTypeTimeseriesVisDataRequest } from '../../../types'; -const isVisDataRequest = ( - request: VisTypeTimeseriesRequest -): request is VisTypeTimeseriesVisDataRequest => { - return !!(request as VisTypeTimeseriesVisDataRequest).body; -}; - -const getTimezoneFromRequest = (request: VisTypeTimeseriesRequest) => { - if (isVisDataRequest(request)) return request.body.timerange.timezone; -}; +export interface SearchCapabilitiesOptions { + timezone?: string; + maxBucketsLimit: number; +} export class DefaultSearchCapabilities { - constructor( - public request: VisTypeTimeseriesRequest, - public fieldsCapabilities: Record = {} - ) {} + public timezone: SearchCapabilitiesOptions['timezone']; + public maxBucketsLimit: SearchCapabilitiesOptions['maxBucketsLimit']; + + constructor(options: SearchCapabilitiesOptions) { + this.timezone = options.timezone; + this.maxBucketsLimit = options.maxBucketsLimit; + } public get defaultTimeInterval() { return null; @@ -55,10 +52,6 @@ export class DefaultSearchCapabilities { }; } - public get searchTimezone() { - return getTimezoneFromRequest(this.request); - } - createUiRestriction(restrictionsObject?: Record) { return { '*': !restrictionsObject, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts index bb79c61d2b032..7426c74dc2426 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.test.ts @@ -8,13 +8,11 @@ import { Unit } from '@elastic/datemath'; import { RollupSearchCapabilities } from './rollup_search_capabilities'; -import { VisTypeTimeseriesRequest } from '../../../types'; describe('Rollup Search Capabilities', () => { const testTimeZone = 'time_zone'; const testInterval = '10s'; const rollupIndex = 'rollupIndex'; - const request = ({} as unknown) as VisTypeTimeseriesRequest; let fieldsCapabilities: Record; let rollupSearchCaps: RollupSearchCapabilities; @@ -33,16 +31,19 @@ describe('Rollup Search Capabilities', () => { }, }; - rollupSearchCaps = new RollupSearchCapabilities(request, fieldsCapabilities, rollupIndex); + rollupSearchCaps = new RollupSearchCapabilities( + { maxBucketsLimit: 2000, timezone: 'UTC' }, + fieldsCapabilities, + rollupIndex + ); }); test('should create instance of RollupSearchRequest', () => { - expect(rollupSearchCaps.fieldsCapabilities).toBe(fieldsCapabilities); expect(rollupSearchCaps.rollupIndex).toBe(rollupIndex); }); test('should return the "timezone" for the rollup request', () => { - expect(rollupSearchCaps.searchTimezone).toBe(testTimeZone); + expect(rollupSearchCaps.timezone).toBe(testTimeZone); }); test('should return the default "interval" for the rollup request', () => { diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts index f4dbccc846908..eafd5a0ee1cf4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/capabilities/rollup_search_capabilities.ts @@ -5,26 +5,28 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { get, has } from 'lodash'; import { leastCommonInterval, isCalendarInterval } from '../lib/interval_helper'; -import { DefaultSearchCapabilities } from './default_search_capabilities'; -import { VisTypeTimeseriesRequest } from '../../../types'; +import { + DefaultSearchCapabilities, + SearchCapabilitiesOptions, +} from './default_search_capabilities'; export class RollupSearchCapabilities extends DefaultSearchCapabilities { rollupIndex: string; availableMetrics: Record; constructor( - req: VisTypeTimeseriesRequest, + options: SearchCapabilitiesOptions, fieldsCapabilities: Record, rollupIndex: string ) { - super(req, fieldsCapabilities); + super(options); this.rollupIndex = rollupIndex; this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {}); + this.timezone = get(this.dateHistogram, 'time_zone', null); } public get dateHistogram() { @@ -46,10 +48,6 @@ export class RollupSearchCapabilities extends DefaultSearchCapabilities { ); } - public get searchTimezone() { - return get(this.dateHistogram, 'time_zone', null); - } - public get whiteListedMetrics() { const baseRestrictions = this.createUiRestriction({ count: this.createUiRestriction(), diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts index a6e7c5b11ee64..f6114a4117bb8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts @@ -25,7 +25,15 @@ class MockSearchStrategy extends AbstractSearchStrategy { } describe('SearchStrategyRegister', () => { - const requestContext = {} as VisTypeTimeseriesRequestHandlerContext; + const requestContext = ({ + core: { + uiSettings: { + client: { + get: jest.fn(), + }, + }, + }, + } as unknown) as VisTypeTimeseriesRequestHandlerContext; let registry: SearchStrategyRegistry; beforeAll(() => { @@ -44,7 +52,7 @@ describe('SearchStrategyRegister', () => { }); test('should return a DefaultSearchStrategy instance', async () => { - const req = {} as VisTypeTimeseriesRequest; + const req = { body: {} } as VisTypeTimeseriesRequest; const { searchStrategy, capabilities } = (await registry.getViableStrategy( requestContext, @@ -65,7 +73,7 @@ describe('SearchStrategyRegister', () => { }); test('should return a MockSearchStrategy instance', async () => { - const req = {} as VisTypeTimeseriesRequest; + const req = { body: {} } as VisTypeTimeseriesRequest; const anotherSearchStrategy = new MockSearchStrategy(); registry.addStrategy(anotherSearchStrategy); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts index fb66e32447c22..7add5cb4a4553 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts @@ -33,6 +33,9 @@ describe('AbstractSearchStrategy', () => { asCurrentUser: jest.fn(), }, }, + uiSettings: { + client: jest.fn(), + }, }, search: { search: jest.fn().mockReturnValue(from(Promise.resolve({}))), diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts index d7a4e6ddedc89..9fa79c7b80f8c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.ts @@ -13,12 +13,23 @@ import { import { DefaultSearchStrategy } from './default_search_strategy'; describe('DefaultSearchStrategy', () => { - const requestContext = {} as VisTypeTimeseriesRequestHandlerContext; + const requestContext = ({ + core: { + uiSettings: { + client: { + get: jest.fn(), + }, + }, + }, + } as unknown) as VisTypeTimeseriesRequestHandlerContext; + let defaultSearchStrategy: DefaultSearchStrategy; let req: VisTypeTimeseriesVisDataRequest; beforeEach(() => { - req = {} as VisTypeTimeseriesVisDataRequest; + req = { + body: {}, + } as VisTypeTimeseriesVisDataRequest; defaultSearchStrategy = new DefaultSearchStrategy(); }); @@ -32,9 +43,11 @@ describe('DefaultSearchStrategy', () => { const value = await defaultSearchStrategy.checkForViability(requestContext, req); expect(value.isViable).toBe(true); - expect(value.capabilities).toEqual({ - request: req, - fieldsCapabilities: {}, - }); + expect(value.capabilities).toMatchInlineSnapshot(` + DefaultSearchCapabilities { + "maxBucketsLimit": undefined, + "timezone": undefined, + } + `); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts index f95bf81b5c1d3..17451f7e5777e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.ts @@ -15,15 +15,21 @@ import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequest, } from '../../../types'; +import { MAX_BUCKETS_SETTING } from '../../../../common/constants'; export class DefaultSearchStrategy extends AbstractSearchStrategy { async checkForViability( requestContext: VisTypeTimeseriesRequestHandlerContext, req: VisTypeTimeseriesRequest ) { + const uiSettings = requestContext.core.uiSettings.client; + return { isViable: true, - capabilities: new DefaultSearchCapabilities(req), + capabilities: new DefaultSearchCapabilities({ + timezone: req.body.timerange?.timezone, + maxBucketsLimit: await uiSettings.get(MAX_BUCKETS_SETTING), + }), }; } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts index e6333ca420e0d..ec6f2a7c21af6 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/rollup_search_strategy.ts @@ -17,6 +17,7 @@ import type { VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesVisDataRequest, } from '../../../types'; +import { MAX_BUCKETS_SETTING } from '../../../../common/constants'; const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData); const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*'); @@ -62,6 +63,7 @@ export class RollupSearchStrategy extends AbstractSearchStrategy { ) { const rollupData = await this.getRollupData(requestContext, indexPatternString); const rollupIndices = getRollupIndices(rollupData); + const uiSettings = requestContext.core.uiSettings.client; isViable = rollupIndices.length === 1; @@ -69,7 +71,13 @@ export class RollupSearchStrategy extends AbstractSearchStrategy { const [rollupIndex] = rollupIndices; const fieldsCapabilities = getCapabilitiesForRollupIndices(rollupData); - capabilities = new RollupSearchCapabilities(req, fieldsCapabilities, rollupIndex); + capabilities = new RollupSearchCapabilities( + { + maxBucketsLimit: await uiSettings.get(MAX_BUCKETS_SETTING), + }, + fieldsCapabilities, + rollupIndex + ); } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js index f9ac4ea2088c0..b9ce76f7176b4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.js @@ -30,6 +30,10 @@ const calculateBucketData = (timeInterval, capabilities) => { bucketSize = 1; } + if (bucketSize > capabilities.maxBucketsLimit) { + bucketSize = capabilities.maxBucketsLimit; + } + // Check decimal if (parsedInterval && parsedInterval.value % 1 !== 0) { if (parsedInterval.unit !== 'ms') { @@ -61,16 +65,16 @@ const calculateBucketSizeForAutoInterval = (req, maxBars) => { return search.aggs.calcAutoIntervalLessThan(maxBars, timerange).asSeconds(); }; -export const getBucketSize = (req, interval, capabilities, maxBars) => { - const bucketSize = calculateBucketSizeForAutoInterval(req, maxBars); - let intervalString = `${bucketSize}s`; +export const getBucketSize = (req, interval, capabilities, bars) => { + const defaultBucketSize = calculateBucketSizeForAutoInterval(req, bars); + let intervalString = `${defaultBucketSize}s`; const gteAutoMatch = Boolean(interval) && interval.match(GTE_INTERVAL_RE); if (gteAutoMatch) { const bucketData = calculateBucketData(gteAutoMatch[1], capabilities); - if (bucketData.bucketSize >= bucketSize) { + if (bucketData.bucketSize >= defaultBucketSize) { return bucketData; } } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js index aa5b55f7e0e7d..2ca77da2cc641 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_bucket_size.test.js @@ -18,43 +18,49 @@ describe('getBucketSize', () => { }, }; + const capabilities = { + timezone: 'UTC', + maxBucketsLimit: 200000, + getValidTimeInterval: jest.fn((v) => v), + }; + test('returns auto calculated buckets', () => { - const result = getBucketSize(req, 'auto', undefined, 100); + const result = getBucketSize(req, 'auto', capabilities, 100); expect(result).toHaveProperty('bucketSize', 30); expect(result).toHaveProperty('intervalString', '30s'); }); test('returns overridden buckets (1s)', () => { - const result = getBucketSize(req, '1s', undefined, 100); + const result = getBucketSize(req, '1s', capabilities, 100); expect(result).toHaveProperty('bucketSize', 1); expect(result).toHaveProperty('intervalString', '1s'); }); test('returns overridden buckets (10m)', () => { - const result = getBucketSize(req, '10m', undefined, 100); + const result = getBucketSize(req, '10m', capabilities, 100); expect(result).toHaveProperty('bucketSize', 600); expect(result).toHaveProperty('intervalString', '10m'); }); test('returns overridden buckets (1d)', () => { - const result = getBucketSize(req, '1d', undefined, 100); + const result = getBucketSize(req, '1d', capabilities, 100); expect(result).toHaveProperty('bucketSize', 86400); expect(result).toHaveProperty('intervalString', '1d'); }); test('returns overridden buckets (>=2d)', () => { - const result = getBucketSize(req, '>=2d', undefined, 100); + const result = getBucketSize(req, '>=2d', capabilities, 100); expect(result).toHaveProperty('bucketSize', 86400 * 2); expect(result).toHaveProperty('intervalString', '2d'); }); test('returns overridden buckets (>=10s)', () => { - const result = getBucketSize(req, '>=10s', undefined, 100); + const result = getBucketSize(req, '>=10s', capabilities, 100); expect(result).toHaveProperty('bucketSize', 30); expect(result).toHaveProperty('intervalString', '30s'); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js index 48b33c1e787e9..22a475a9997a7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js @@ -32,7 +32,7 @@ export function dateHistogram( barTargetUiSettings ); const { from, to } = getTimerange(req); - const timezone = capabilities.searchTimezone; + const { timezone } = capabilities; overwrite(doc, `aggs.${annotation.id}.date_histogram`, { field: timeField, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index dab9a24d06c0f..a9b4f99fdb693 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -37,7 +37,7 @@ export function dateHistogram( const getDateHistogramForLastBucketMode = () => { const { from, to } = offsetTime(req, series.offset_time); - const timezone = capabilities.searchTimezone; + const { timezone } = capabilities; overwrite(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { field: timeField, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js index 945c57b2341f3..a1fd242dc150e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.test.js @@ -23,7 +23,6 @@ describe('dateHistogram(req, panel, series)', () => { req = { body: { timerange: { - timezone: 'UTC', min: '2017-01-01T00:00:00Z', max: '2017-01-01T01:00:00Z', }, @@ -40,7 +39,7 @@ describe('dateHistogram(req, panel, series)', () => { queryStringOptions: {}, }; indexPattern = {}; - capabilities = new DefaultSearchCapabilities(req); + capabilities = new DefaultSearchCapabilities({ timezone: 'UTC', maxBucketsLimit: 2000 }); uiSettings = { get: async (key) => (key === UI_SETTINGS.HISTOGRAM_MAX_BARS ? 100 : 50), }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js index 7f126a39e4f3c..db5767f29d090 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.test.js @@ -50,7 +50,7 @@ describe('metricBuckets(req, panel, series)', () => { }, {}, {}, - undefined, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, { get: async () => 50, } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js index cecfee7c660f5..b79e8de13062c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.test.js @@ -46,14 +46,30 @@ describe('positiveRate(req, panel, series)', () => { test('calls next when finished', async () => { const next = jest.fn(); - await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + await positiveRate( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns positive rate aggs', async () => { const next = (doc) => doc; - const doc = await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + const doc = await positiveRate( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(doc).toEqual({ aggs: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js index 8c9179f12824d..16cb01c510bf3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.test.js @@ -51,13 +51,29 @@ describe('siblingBuckets(req, panel, series)', () => { test('calls next when finished', async () => { const next = jest.fn(); - await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + await siblingBuckets( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(next.mock.calls.length).toEqual(1); }); test('returns sibling aggs', async () => { const next = (doc) => doc; - const doc = await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({}); + const doc = await siblingBuckets( + req, + panel, + series, + {}, + {}, + { maxBucketsLimit: 2000, getValidTimeInterval: jest.fn(() => '1d') }, + uiSettings + )(next)({}); expect(doc).toEqual({ aggs: { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index 5518065643172..aff1bd5041be5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -32,7 +32,7 @@ export function dateHistogram(req, panel, esQueryConfig, indexPattern, capabilit barTargetUiSettings ); const { from, to } = getTimerange(req); - const timezone = capabilities.searchTimezone; + const { timezone } = capabilities; panel.series.forEach((column) => { const aggRoot = calculateAggRoot(doc, column); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts index d97af8ac748f4..89d87da5f3d7e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/series/build_request_body.test.ts @@ -72,7 +72,7 @@ describe('buildRequestBody(req)', () => { const series = panel.series[0]; const getValidTimeInterval = jest.fn(() => '10s'); const capabilities = { - searchTimezone: 'UTC', + timezone: 'UTC', getValidTimeInterval, }; const config = { diff --git a/src/plugins/vis_type_timeseries/server/types.ts b/src/plugins/vis_type_timeseries/server/types.ts index 7b42cf61d52b3..2fc46b7cd1f11 100644 --- a/src/plugins/vis_type_timeseries/server/types.ts +++ b/src/plugins/vis_type_timeseries/server/types.ts @@ -23,7 +23,7 @@ export type ConfigObservable = Observable; export type VisTypeTimeseriesRequestHandlerContext = DataRequestHandlerContext; export type VisTypeTimeseriesRouter = IRouter; export type VisTypeTimeseriesVisDataRequest = KibanaRequest<{}, {}, VisPayload>; -export type VisTypeTimeseriesFieldsRequest = KibanaRequest<{}, { index: string }, {}>; +export type VisTypeTimeseriesFieldsRequest = KibanaRequest<{}, { index: string }, any>; export type VisTypeTimeseriesRequest = | VisTypeTimeseriesFieldsRequest | VisTypeTimeseriesVisDataRequest; diff --git a/src/plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts index 7e3ff8226fbb6..fa463bea6f27f 100644 --- a/src/plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -120,6 +120,7 @@ export const gaugeVisTypeDefinition: VisTypeDefinition = { '!cumulative_sum', '!geo_bounds', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts index 468651bb4cf4c..e594122871fe7 100644 --- a/src/plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -84,6 +84,7 @@ export const goalVisTypeDefinition: VisTypeDefinition = { '!cumulative_sum', '!geo_bounds', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index 8d538399f68b2..f3f320b3658a0 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -95,6 +95,7 @@ export const heatmapVisTypeDefinition: VisTypeDefinition = { 'std_dev', 'top_hits', '!filtered_metric', + '!single_percentile', ], defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/area.ts b/src/plugins/vis_type_xy/public/vis_types/area.ts index dfe9bc2f42b84..f22f8df1752d6 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.ts +++ b/src/plugins/vis_type_xy/public/vis_types/area.ts @@ -133,7 +133,7 @@ export const getAreaVisTypeDefinition = ( title: i18n.translate('visTypeXy.area.metricsTitle', { defaultMessage: 'Y-axis', }), - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], min: 1, defaults: [{ schema: 'metric', type: 'count' }], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.ts b/src/plugins/vis_type_xy/public/vis_types/histogram.ts index ba20502a3b9af..732833ffecc80 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts @@ -137,7 +137,7 @@ export const getHistogramVisTypeDefinition = ( defaultMessage: 'Y-axis', }), min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], defaults: [{ schema: 'metric', type: 'count' }], }, { diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts index 62da0448e56bd..791d93bb646b2 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts @@ -136,7 +136,7 @@ export const getHorizontalBarVisTypeDefinition = ( defaultMessage: 'Y-axis', }), min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], defaults: [{ schema: 'metric', type: 'count' }], }, { diff --git a/src/plugins/vis_type_xy/public/vis_types/line.ts b/src/plugins/vis_type_xy/public/vis_types/line.ts index 5a9eb5198df35..6316fe4458229 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.ts +++ b/src/plugins/vis_type_xy/public/vis_types/line.ts @@ -132,7 +132,7 @@ export const getLineVisTypeDefinition = ( name: 'metric', title: i18n.translate('visTypeXy.line.metricTitle', { defaultMessage: 'Y-axis' }), min: 1, - aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'], + aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric', '!single_percentile'], defaults: [{ schema: 'metric', type: 'count' }], }, { diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index 989a9bf5d2cb7..e2e2a4c089270 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -67,7 +67,10 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe indexPatterns = [vis.data.indexPattern]; } - const editable = getCapabilities().visualize.save as boolean; + const capabilities = { + visualizeSave: Boolean(getCapabilities().visualize.save), + dashboardSave: Boolean(getCapabilities().dashboard?.showWriteControls), + }; return new VisualizeEmbeddable( getTimeFilter(), @@ -76,8 +79,8 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe indexPatterns, editPath, editUrl, - editable, deps, + capabilities, }, input, attributeService, diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 580ffef548fe1..429dabeeef042 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -50,7 +50,7 @@ export interface VisualizeEmbeddableConfiguration { indexPatterns?: IIndexPattern[]; editPath: string; editUrl: string; - editable: boolean; + capabilities: { visualizeSave: boolean; dashboardSave: boolean }; deps: VisualizeEmbeddableFactoryDeps; } @@ -111,7 +111,7 @@ export class VisualizeEmbeddable constructor( timefilter: TimefilterContract, - { vis, editPath, editUrl, indexPatterns, editable, deps }: VisualizeEmbeddableConfiguration, + { vis, editPath, editUrl, indexPatterns, deps, capabilities }: VisualizeEmbeddableConfiguration, initialInput: VisualizeInput, attributeService?: AttributeService< VisualizeSavedObjectAttributes, @@ -129,7 +129,6 @@ export class VisualizeEmbeddable editApp: 'visualize', editUrl, indexPatterns, - editable, visTypeName: vis.type.name, }, parent @@ -143,6 +142,12 @@ export class VisualizeEmbeddable this.attributeService = attributeService; this.savedVisualizationsLoader = savedVisualizationsLoader; + if (this.attributeService) { + const isByValue = !this.inputIsRefType(initialInput); + const editable = capabilities.visualizeSave || (isByValue && capabilities.dashboardSave); + this.updateOutput({ ...this.getOutput(), editable }); + } + this.subscriptions.push( this.getUpdated$().subscribe(() => { const isDirty = this.handleChanges(); diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 4f5679a14b0b7..e696bcb5dbe4d 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -82,6 +82,7 @@ export const getTopNavConfig = ( setActiveUrl, toastNotifications, visualizeCapabilities, + dashboardCapabilities, i18n: { Context: I18nContext }, dashboard, savedObjectsTagging, @@ -205,9 +206,9 @@ export const getTopNavConfig = ( } }; + const allowByValue = dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; const saveButtonLabel = - embeddableId || - (!savedVis.id && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && originatingApp) + embeddableId || (!savedVis.id && allowByValue && originatingApp) ? i18n.translate('visualize.topNavMenu.saveVisualizationToLibraryButtonLabel', { defaultMessage: 'Save to library', }) @@ -219,9 +220,11 @@ export const getTopNavConfig = ( defaultMessage: 'Save', }); - const showSaveAndReturn = - originatingApp && - (savedVis?.id || dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables); + const showSaveAndReturn = originatingApp && (savedVis?.id || allowByValue); + + const showSaveButton = + visualizeCapabilities.save || + (allowByValue && !showSaveAndReturn && dashboardCapabilities.showWriteControls); const topNavMenu: TopNavMenuData[] = [ { @@ -300,7 +303,7 @@ export const getTopNavConfig = ( }, ] : []), - ...(visualizeCapabilities.save + ...(showSaveButton ? [ { id: 'save', @@ -439,7 +442,12 @@ export const getTopNavConfig = ( /> ) : ( { defaultMessage: 'Read only', }), tooltip: i18n.translate('visualize.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save visualizations', + defaultMessage: 'Unable to save visualizations to the library', }), iconType: 'glasses', }); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 14bd002ec9487..699165a51ca8c 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -28,6 +28,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickLinkText(text: string) { await find.clickByDisplayedLinkText(text); } + async clickKibanaSettings() { await testSubjects.click('settings'); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -89,6 +90,22 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } + async setAdvancedSettingsTextArea(propertyName: string, propertyValue: string) { + const wrapper = await testSubjects.find(`advancedSetting-editField-${propertyName}`); + const textarea = await wrapper.findByTagName('textarea'); + await textarea.focus(); + // only way to properly replace the value of the ace editor is via the JS api + await browser.execute( + (editor: string, value: string) => { + return (window as any).ace.edit(editor).setValue(value); + }, + `advancedSetting-editField-${propertyName}-editor`, + propertyValue + ); + await testSubjects.click(`advancedSetting-saveButton`); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + async toggleAdvancedSettingCheckbox(propertyName: string) { await testSubjects.click(`advancedSetting-editField-${propertyName}`); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -162,6 +179,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async sortBy(columnName: string) { const chartTypes = await find.allByCssSelector('table.euiTable thead tr th button'); + async function getChartType(chart: Record) { const chartString = await chart.getVisibleText(); if (chartString === columnName) { @@ -169,6 +187,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } } + const getChartTypesPromises = chartTypes.map(getChartType); return Promise.all(getChartTypesPromises); } diff --git a/tsconfig.json b/tsconfig.json index 18647153acb0a..c852481518d54 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -123,6 +123,7 @@ { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./x-pack/plugins/timelines/tsconfig.json" }, { "path": "./x-pack/plugins/transform/tsconfig.json" }, { "path": "./x-pack/plugins/translations/tsconfig.json" }, { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 663ae32f9128a..6bbbf6cd6b82d 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -53,6 +53,7 @@ "xpack.spaces": "plugins/spaces", "xpack.savedObjectsTagging": ["plugins/saved_objects_tagging"], "xpack.taskManager": "legacy/plugins/task_manager", + "xpack.timelines": "plugins/timelines", "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index a9a0149e72ce7..e340f8bf19126 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -12,6 +12,7 @@ "infra" ], "optionalPlugins": [ + "spaces", "cloud", "usageCollection", "taskManager", diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx index ef67501ec761b..1e368b2eb5368 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/EmbeddedMap.tsx @@ -24,7 +24,7 @@ import { import { useLayerList } from './useLayerList'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import type { RenderTooltipContentParams } from '../../../../../../maps/public'; import { MapToolTip } from './MapToolTip'; import { useMapFilters } from './useMapFilters'; import { EmbeddableStart } from '../../../../../../../../src/plugins/embeddable/public'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx index 7e6c8ddd493bf..7501d5bfaa2c5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/MapToolTip.tsx @@ -20,7 +20,7 @@ import { TRANSACTION_DURATION_COUNTRY, TRANSACTION_DURATION_REGION, } from './useLayerList'; -import { RenderTooltipContentParams } from '../../../../../../maps/public'; +import type { RenderTooltipContentParams } from '../../../../../../maps/public'; import { I18LABELS } from '../translations'; type MapToolTipProps = Partial; diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts index 1c33fcbd71dac..19163da449b90 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.test.ts @@ -36,7 +36,12 @@ describe('createStaticIndexPattern', () => { 'xpack.apm.autocreateApmIndexPattern': false, }); const savedObjectsClient = getMockSavedObjectsClient(); - await createStaticIndexPattern(setup, context, savedObjectsClient); + await createStaticIndexPattern( + setup, + context, + savedObjectsClient, + 'default' + ); expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); @@ -53,7 +58,12 @@ describe('createStaticIndexPattern', () => { const savedObjectsClient = getMockSavedObjectsClient(); - await createStaticIndexPattern(setup, context, savedObjectsClient); + await createStaticIndexPattern( + setup, + context, + savedObjectsClient, + 'default' + ); expect(savedObjectsClient.create).not.toHaveBeenCalled(); }); @@ -70,7 +80,12 @@ describe('createStaticIndexPattern', () => { const savedObjectsClient = getMockSavedObjectsClient(); - await createStaticIndexPattern(setup, context, savedObjectsClient); + await createStaticIndexPattern( + setup, + context, + savedObjectsClient, + 'default' + ); expect(savedObjectsClient.create).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts index 0b7f82c0b8388..b91fb8342a212 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/create_static_index_pattern.ts @@ -20,7 +20,8 @@ import { getApmIndexPatternTitle } from './get_apm_index_pattern_title'; export async function createStaticIndexPattern( setup: Setup, context: APMRequestHandlerContext, - savedObjectsClient: InternalSavedObjectsClient + savedObjectsClient: InternalSavedObjectsClient, + spaceId: string | undefined ): Promise { return withApmSpan('create_static_index_pattern', async () => { const { config } = context; @@ -46,7 +47,11 @@ export async function createStaticIndexPattern( ...apmIndexPattern.attributes, title: apmIndexPatternTitle, }, - { id: APM_STATIC_INDEX_PATTERN_ID, overwrite: false } + { + id: APM_STATIC_INDEX_PATTERN_ID, + overwrite: false, + namespace: spaceId, + } ) ); return true; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f556374179c51..db96794627519 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -16,6 +16,7 @@ import { Plugin, PluginInitializerContext, } from 'src/core/server'; +import { SpacesPluginSetup } from '../../spaces/server'; import { APMConfig, APMXPackConfig } from '.'; import { mergeConfigs } from './index'; import { APMOSSPluginSetup } from '../../../../src/plugins/apm_oss/server'; @@ -65,6 +66,7 @@ export class APMPlugin implements Plugin { public setup( core: CoreSetup, plugins: { + spaces?: SpacesPluginSetup; apmOss: APMOSSPluginSetup; home: HomeServerPluginSetup; licensing: LicensingPluginSetup; @@ -148,11 +150,7 @@ export class APMPlugin implements Plugin { createApmApi().init(core, { config$: mergedConfig$, logger: this.logger!, - plugins: { - observability: plugins.observability, - security: plugins.security, - ml: plugins.ml, - }, + plugins, }); const boundGetApmIndices = async () => diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index fd7d2120ab6f5..3b800c23135ce 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -21,10 +21,13 @@ export const staticIndexPatternRoute = createRoute((core) => ({ getInternalSavedObjectsClient(core), ]); + const spaceId = context.plugins.spaces?.spacesService.getSpaceId(request); + const didCreateIndexPattern = await createStaticIndexPattern( setup, context, - savedObjectsClient + savedObjectsClient, + spaceId ); return { created: didCreateIndexPattern }; diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 1575041fb2f45..3ba24b4ed5268 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -14,6 +14,7 @@ import { } from 'src/core/server'; import { Observable } from 'rxjs'; import { RequiredKeys, DeepPartial } from 'utility-types'; +import { SpacesPluginStart } from '../../../spaces/server'; import { ObservabilityPluginSetup } from '../../../observability/server'; import { LicensingApiRequestHandlerContext } from '../../../licensing/server'; import { SecurityPluginSetup } from '../../../security/server'; @@ -93,6 +94,7 @@ export type APMRequestHandlerContext< config: APMConfig; logger: Logger; plugins: { + spaces?: SpacesPluginStart; observability?: ObservabilityPluginSetup; security?: SecurityPluginSetup; ml?: MlPluginSetup; diff --git a/x-pack/plugins/banners/README.md b/x-pack/plugins/banners/README.md index 890c194e1bcb0..6ef2d9196f379 100644 --- a/x-pack/plugins/banners/README.md +++ b/x-pack/plugins/banners/README.md @@ -12,7 +12,7 @@ The options are The placement of the banner. The allowed values are: - `disabled` - The banner will be disabled - - `header` - The banner will be displayed in the header + - `top` - The banner will be displayed in the header - `textContent` @@ -31,7 +31,7 @@ The color for the banner's background. Must be a valid hex color `kibana.yml` ```yaml xpack.banners: - placement: 'header' + placement: 'top' textContent: 'Production environment - Proceed with **special levels** of caution' textColor: '#FF0000' backgroundColor: '#CC2211' diff --git a/x-pack/plugins/banners/common/types.ts b/x-pack/plugins/banners/common/types.ts index 0c785f516ddb3..6c16b4e8055bb 100644 --- a/x-pack/plugins/banners/common/types.ts +++ b/x-pack/plugins/banners/common/types.ts @@ -10,7 +10,7 @@ export interface BannerInfoResponse { banner: BannerConfiguration; } -export type BannerPlacement = 'disabled' | 'header'; +export type BannerPlacement = 'disabled' | 'top'; export interface BannerConfiguration { placement: BannerPlacement; diff --git a/x-pack/plugins/banners/public/components/banner.tsx b/x-pack/plugins/banners/public/components/banner.tsx index ea30e46881d0c..ae28986297659 100644 --- a/x-pack/plugins/banners/public/components/banner.tsx +++ b/x-pack/plugins/banners/public/components/banner.tsx @@ -26,7 +26,7 @@ export const Banner: FC = ({ bannerConfig }) => { }} >
- +
); diff --git a/x-pack/plugins/banners/public/plugin.test.tsx b/x-pack/plugins/banners/public/plugin.test.tsx index 036ad17e2598e..8722d9516b9db 100644 --- a/x-pack/plugins/banners/public/plugin.test.tsx +++ b/x-pack/plugins/banners/public/plugin.test.tsx @@ -7,11 +7,19 @@ import { getBannerInfoMock } from './plugin.test.mocks'; import { coreMock } from '../../../../src/core/public/mocks'; +import { BannerConfiguration } from '../common/types'; import { BannersPlugin } from './plugin'; -import { BannerClientConfig } from './types'; const nextTick = async () => await new Promise((resolve) => resolve()); +const createBannerConfig = (parts: Partial = {}): BannerConfiguration => ({ + placement: 'disabled', + textContent: 'foo', + textColor: '#FFFFFF', + backgroundColor: '#000000', + ...parts, +}); + describe('BannersPlugin', () => { let plugin: BannersPlugin; let pluginInitContext: ReturnType; @@ -25,11 +33,12 @@ describe('BannersPlugin', () => { getBannerInfoMock.mockResolvedValue({ allowed: false, + banner: createBannerConfig(), }); }); - const startPlugin = async (config: BannerClientConfig) => { - pluginInitContext = coreMock.createPluginInitializerContext(config); + const startPlugin = async () => { + pluginInitContext = coreMock.createPluginInitializerContext(); plugin = new BannersPlugin(pluginInitContext); plugin.setup(coreSetup); plugin.start(coreStart); @@ -41,46 +50,62 @@ describe('BannersPlugin', () => { getBannerInfoMock.mockReset(); }); - it('calls `getBannerInfo` if `config.placement !== disabled`', async () => { - await startPlugin({ - placement: 'header', + describe('when banner is allowed', () => { + it('registers the header banner if `banner.placement` is `top`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: true, + banner: createBannerConfig({ + placement: 'top', + }), + }); + + await startPlugin(); + + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(1); + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledWith({ + content: expect.any(Function), + }); }); - expect(getBannerInfoMock).toHaveBeenCalledTimes(1); - }); + it('does not register the header banner if `banner.placement` is `disabled`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: true, + banner: createBannerConfig({ + placement: 'disabled', + }), + }); - it('does not call `getBannerInfo` if `config.placement === disabled`', async () => { - await startPlugin({ - placement: 'disabled', - }); + await startPlugin(); - expect(getBannerInfoMock).not.toHaveBeenCalled(); + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(0); + }); }); - it('registers the header banner if `getBannerInfo` return `allowed=true`', async () => { - getBannerInfoMock.mockResolvedValue({ - allowed: true, - }); + describe('when banner is not allowed', () => { + it('does not register the header banner if `banner.placement` is `top`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: false, + banner: createBannerConfig({ + placement: 'top', + }), + }); - await startPlugin({ - placement: 'header', - }); + await startPlugin(); - expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(1); - expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledWith({ - content: expect.any(Function), + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(0); }); - }); - it('does not register the header banner if `getBannerInfo` return `allowed=false`', async () => { - getBannerInfoMock.mockResolvedValue({ - allowed: false, - }); + it('does not register the header banner if `banner.placement` is `disabled`', async () => { + getBannerInfoMock.mockResolvedValue({ + allowed: false, + banner: createBannerConfig({ + placement: 'disabled', + }), + }); - await startPlugin({ - placement: 'header', - }); + await startPlugin(); - expect(coreStart.chrome.setHeaderBanner).not.toHaveBeenCalled(); + expect(coreStart.chrome.setHeaderBanner).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/x-pack/plugins/banners/public/plugin.tsx b/x-pack/plugins/banners/public/plugin.tsx index dca99a816a25b..014d2de58b9ea 100644 --- a/x-pack/plugins/banners/public/plugin.tsx +++ b/x-pack/plugins/banners/public/plugin.tsx @@ -9,35 +9,28 @@ import React from 'react'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; import { Banner } from './components'; -import { BannerClientConfig } from './types'; import { getBannerInfo } from './get_banner_info'; export class BannersPlugin implements Plugin<{}, {}, {}, {}> { - private readonly config: BannerClientConfig; - - constructor(context: PluginInitializerContext) { - this.config = context.config.get(); - } + constructor(context: PluginInitializerContext) {} setup({}: CoreSetup<{}, {}>) { return {}; } start({ chrome, uiSettings, http }: CoreStart) { - if (this.config.placement !== 'disabled') { - getBannerInfo(http).then( - ({ allowed, banner }) => { - if (allowed) { - chrome.setHeaderBanner({ - content: toMountPoint(), - }); - } - }, - () => { - chrome.setHeaderBanner(undefined); + getBannerInfo(http).then( + ({ allowed, banner }) => { + if (allowed && banner.placement === 'top') { + chrome.setHeaderBanner({ + content: toMountPoint(), + }); } - ); - } + }, + () => { + chrome.setHeaderBanner(undefined); + } + ); return {}; } diff --git a/x-pack/plugins/banners/server/config.ts b/x-pack/plugins/banners/server/config.ts index 9a8cc9680c296..ec1c7006a84ce 100644 --- a/x-pack/plugins/banners/server/config.ts +++ b/x-pack/plugins/banners/server/config.ts @@ -5,12 +5,13 @@ * 2.0. */ +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from 'kibana/server'; import { isHexColor } from './utils'; const configSchema = schema.object({ - placement: schema.oneOf([schema.literal('disabled'), schema.literal('header')], { + placement: schema.oneOf([schema.literal('disabled'), schema.literal('top')], { defaultValue: 'disabled', }), textContent: schema.string({ defaultValue: '' }), @@ -30,13 +31,25 @@ const configSchema = schema.object({ }, defaultValue: '#FFF9E8', }), + disableSpaceBanners: schema.boolean({ defaultValue: false }), }); export type BannersConfigType = TypeOf; export const config: PluginConfigDescriptor = { schema: configSchema, - exposeToBrowser: { - placement: true, - }, + exposeToBrowser: {}, + deprecations: () => [ + (rootConfig, fromPath, addDeprecation) => { + const pluginConfig = get(rootConfig, fromPath); + if (pluginConfig?.placement === 'header') { + addDeprecation({ + message: 'The `header` value for xpack.banners.placement has been replaced by `top`', + }); + pluginConfig.placement = 'top'; + } + + return rootConfig; + }, + ], }; diff --git a/x-pack/plugins/banners/server/plugin.test.mocks.ts b/x-pack/plugins/banners/server/plugin.test.mocks.ts new file mode 100644 index 0000000000000..316699c2c2020 --- /dev/null +++ b/x-pack/plugins/banners/server/plugin.test.mocks.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const registerRoutesMock = jest.fn(); +jest.doMock('./routes', () => ({ + registerRoutes: registerRoutesMock, +})); + +export const registerSettingsMock = jest.fn(); +jest.doMock('./ui_settings', () => ({ + registerSettings: registerSettingsMock, +})); diff --git a/x-pack/plugins/banners/server/plugin.test.ts b/x-pack/plugins/banners/server/plugin.test.ts new file mode 100644 index 0000000000000..b3f8c7be696cd --- /dev/null +++ b/x-pack/plugins/banners/server/plugin.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { registerRoutesMock, registerSettingsMock } from './plugin.test.mocks'; + +import { coreMock } from '../../../../src/core/server/mocks'; +import { BannersPlugin } from './plugin'; +import { BannersConfigType } from './config'; + +describe('BannersPlugin', () => { + let plugin: BannersPlugin; + let pluginInitContext: ReturnType; + let coreSetup: ReturnType; + let bannerConfig: BannersConfigType; + + beforeEach(() => { + bannerConfig = { + placement: 'top', + textContent: 'foo', + backgroundColor: '#000000', + textColor: '#FFFFFF', + disableSpaceBanners: false, + }; + pluginInitContext = coreMock.createPluginInitializerContext(); + pluginInitContext.config.get.mockReturnValue(bannerConfig); + coreSetup = coreMock.createSetup(); + + plugin = new BannersPlugin(pluginInitContext); + }); + + afterEach(() => { + registerRoutesMock.mockReset(); + registerSettingsMock.mockReset(); + }); + + describe('#setup', () => { + it('calls `registerRoutes` with the correct parameters', () => { + plugin.setup(coreSetup); + + expect(registerRoutesMock).toHaveBeenCalledTimes(1); + expect(registerRoutesMock).toHaveBeenCalledWith(expect.any(Object), bannerConfig); + }); + it('calls `registerSettings` with the correct parameters', () => { + plugin.setup(coreSetup); + + expect(registerSettingsMock).toHaveBeenCalledTimes(1); + expect(registerSettingsMock).toHaveBeenCalledWith(coreSetup.uiSettings, bannerConfig); + }); + }); +}); diff --git a/x-pack/plugins/banners/server/plugin.ts b/x-pack/plugins/banners/server/plugin.ts index 66cd083189975..852ba135c478b 100644 --- a/x-pack/plugins/banners/server/plugin.ts +++ b/x-pack/plugins/banners/server/plugin.ts @@ -6,21 +6,22 @@ */ import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; -import { BannerConfiguration } from '../common'; import { BannersConfigType } from './config'; import { BannersRequestHandlerContext } from './types'; import { registerRoutes } from './routes'; +import { registerSettings } from './ui_settings'; export class BannersPlugin implements Plugin<{}, {}, {}, {}> { - private readonly config: BannerConfiguration; + private readonly config: BannersConfigType; constructor(context: PluginInitializerContext) { - this.config = convertConfig(context.config.get()); + this.config = context.config.get(); } setup({ uiSettings, getStartServices, http }: CoreSetup<{}, {}>) { const router = http.createRouter(); registerRoutes(router, this.config); + registerSettings(uiSettings, this.config); return {}; } @@ -29,5 +30,3 @@ export class BannersPlugin implements Plugin<{}, {}, {}, {}> { return {}; } } - -const convertConfig = (raw: BannersConfigType): BannerConfiguration => raw; diff --git a/x-pack/plugins/banners/server/routes/index.ts b/x-pack/plugins/banners/server/routes/index.ts index a4eedc3234c86..347236b4df4e2 100644 --- a/x-pack/plugins/banners/server/routes/index.ts +++ b/x-pack/plugins/banners/server/routes/index.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { BannerConfiguration } from '../../common'; +import { BannersConfigType } from '../config'; import { BannersRouter } from '../types'; import { registerInfoRoute } from './info'; -export const registerRoutes = (router: BannersRouter, config: BannerConfiguration) => { +export const registerRoutes = (router: BannersRouter, config: BannersConfigType) => { registerInfoRoute(router, config); }; diff --git a/x-pack/plugins/banners/server/routes/info.ts b/x-pack/plugins/banners/server/routes/info.ts index e0db842028c37..806c4a7ae6b39 100644 --- a/x-pack/plugins/banners/server/routes/info.ts +++ b/x-pack/plugins/banners/server/routes/info.ts @@ -5,26 +5,33 @@ * 2.0. */ +import { IUiSettingsClient } from 'kibana/server'; import { ILicense } from '../../../licensing/server'; -import { BannerInfoResponse, BannerConfiguration } from '../../common'; +import { BannersConfigType } from '../config'; +import { BannerInfoResponse, BannerConfiguration, BannerPlacement } from '../../common'; import { BannersRouter } from '../types'; -export const registerInfoRoute = (router: BannersRouter, config: BannerConfiguration) => { +export const registerInfoRoute = (router: BannersRouter, config: BannersConfigType) => { router.get( { path: '/api/banners/info', validate: false, options: { - authRequired: false, + authRequired: 'optional', }, }, - (ctx, req, res) => { + async (ctx, req, res) => { const allowed = isValidLicense(ctx.licensing.license); + const bannerConfig = + req.auth.isAuthenticated && config.disableSpaceBanners === false + ? await getBannerConfig(ctx.core.uiSettings.client) + : config; + return res.ok({ body: { allowed, - banner: config, + banner: bannerConfig, } as BannerInfoResponse, }); } @@ -34,3 +41,19 @@ export const registerInfoRoute = (router: BannersRouter, config: BannerConfigura const isValidLicense = (license: ILicense): boolean => { return license.hasAtLeast('gold'); }; + +const getBannerConfig = async (client: IUiSettingsClient): Promise => { + const [placement, textContent, textColor, backgroundColor] = await Promise.all([ + client.get('banners:placement'), + client.get('banners:textContent'), + client.get('banners:textColor'), + client.get('banners:backgroundColor'), + ]); + + return { + placement, + textContent, + textColor, + backgroundColor, + }; +}; diff --git a/x-pack/plugins/banners/server/ui_settings.test.ts b/x-pack/plugins/banners/server/ui_settings.test.ts new file mode 100644 index 0000000000000..9fae019774336 --- /dev/null +++ b/x-pack/plugins/banners/server/ui_settings.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { uiSettingsServiceMock } from '../../../../src/core/server/mocks'; +import { BannersConfigType } from './config'; +import { registerSettings } from './ui_settings'; + +const createConfig = (parts: Partial = {}): BannersConfigType => ({ + placement: 'disabled', + backgroundColor: '#0000', + textColor: '#FFFFFF', + textContent: 'Hello from the banner', + disableSpaceBanners: false, + ...parts, +}); + +describe('registerSettings', () => { + let uiSettings: ReturnType; + + beforeEach(() => { + uiSettings = uiSettingsServiceMock.createSetupContract(); + }); + + it('registers the settings', () => { + registerSettings(uiSettings, createConfig()); + + expect(uiSettings.register).toHaveBeenCalledTimes(1); + expect(uiSettings.register).toHaveBeenCalledWith({ + 'banners:placement': expect.any(Object), + 'banners:textContent': expect.any(Object), + 'banners:textColor': expect.any(Object), + 'banners:backgroundColor': expect.any(Object), + }); + }); + + it('does not register the settings if `config.disableSpaceBanners` is `true`', () => { + registerSettings(uiSettings, createConfig({ disableSpaceBanners: true })); + + expect(uiSettings.register).not.toHaveBeenCalled(); + }); + + it('uses the configuration values as defaults', () => { + const config = createConfig({ + placement: 'top', + backgroundColor: '#FF00CC', + textColor: '#AAFFEE', + textContent: 'Some text', + }); + + registerSettings(uiSettings, config); + + expect(uiSettings.register).toHaveBeenCalledTimes(1); + expect(uiSettings.register).toHaveBeenCalledWith({ + 'banners:placement': expect.objectContaining({ + value: config.placement, + }), + 'banners:textContent': expect.objectContaining({ + value: config.textContent, + }), + 'banners:textColor': expect.objectContaining({ + value: config.textColor, + }), + 'banners:backgroundColor': expect.objectContaining({ + value: config.backgroundColor, + }), + }); + }); +}); diff --git a/x-pack/plugins/banners/server/ui_settings.ts b/x-pack/plugins/banners/server/ui_settings.ts new file mode 100644 index 0000000000000..d35ab76c41a58 --- /dev/null +++ b/x-pack/plugins/banners/server/ui_settings.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { UiSettingsServiceSetup } from 'src/core/server'; +import { BannersConfigType } from './config'; +import { isHexColor } from './utils'; + +export const registerSettings = (uiSettings: UiSettingsServiceSetup, config: BannersConfigType) => { + if (config.disableSpaceBanners) { + return; + } + + const subscriptionLink = ` + + ${i18n.translate('xpack.banners.settings.subscriptionRequiredLink.text', { + defaultMessage: 'Subscription required.', + })} + + `; + + uiSettings.register({ + 'banners:placement': { + name: i18n.translate('xpack.banners.settings.placement.title', { + defaultMessage: 'Banner placement', + }), + description: i18n.translate('xpack.banners.settings.placement.description', { + defaultMessage: + 'Display a top banner for this space, above the Elastic header. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + category: ['banner'], + order: 1, + type: 'select', + value: config.placement, + options: ['disabled', 'top'], + optionLabels: { + disabled: i18n.translate('xpack.banners.settings.placement.disabled', { + defaultMessage: 'Disabled', + }), + top: i18n.translate('xpack.banners.settings.placement.top', { + defaultMessage: 'Top', + }), + }, + requiresPageReload: true, + schema: schema.oneOf([schema.literal('disabled'), schema.literal('top')]), + }, + 'banners:textContent': { + name: i18n.translate('xpack.banners.settings.textContent.title', { + defaultMessage: 'Banner text', + }), + description: i18n.translate('xpack.banners.settings.text.description', { + defaultMessage: 'Add Markdown-formatted text to the banner. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + sensitive: true, + category: ['banner'], + order: 2, + type: 'markdown', + value: config.textContent, + requiresPageReload: true, + schema: schema.string(), + }, + 'banners:textColor': { + name: i18n.translate('xpack.banners.settings.textColor.title', { + defaultMessage: 'Banner text color', + }), + description: i18n.translate('xpack.banners.settings.textColor.description', { + defaultMessage: 'Set the color of the banner text. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + category: ['banner'], + order: 3, + type: 'color', + value: config.textColor, + requiresPageReload: true, + schema: schema.string({ + validate: (color) => { + if (!isHexColor(color)) { + return `'banners:textColor' must be an hex color`; + } + }, + }), + }, + 'banners:backgroundColor': { + name: i18n.translate('xpack.banners.settings.backgroundColor.title', { + defaultMessage: 'Banner background color', + }), + description: i18n.translate('xpack.banners.settings.backgroundColor.description', { + defaultMessage: 'Set the background color for the banner. {subscriptionLink}', + values: { + subscriptionLink, + }, + }), + category: ['banner'], + order: 4, + type: 'color', + value: config.backgroundColor, + requiresPageReload: true, + schema: schema.string({ + validate: (color) => { + if (!isHexColor(color)) { + return `'banners:backgroundColor' must be an hex color`; + } + }, + }), + }, + }); +}; diff --git a/x-pack/plugins/canvas/scripts/shareable_runtime.js b/x-pack/plugins/canvas/scripts/shareable_runtime.js index 7f7f6d235c984..b760a92811b8e 100644 --- a/x-pack/plugins/canvas/scripts/shareable_runtime.js +++ b/x-pack/plugins/canvas/scripts/shareable_runtime.js @@ -56,7 +56,7 @@ run( 'webpack-dev-server', '--config', webpackConfig, - ...(process.stdout.isTTY ? ['--progress'] : []), + ...(process.stdout.isTTY && !process.env.CI ? ['--progress'] : []), '--hide-modules', '--display-entrypoints', 'false', @@ -93,7 +93,7 @@ run( '--config', webpackConfig, '--hide-modules', - ...(process.stdout.isTTY ? ['--progress'] : []), + ...(process.stdout.isTTY && !process.env.CI ? ['--progress'] : []), ], { ...options, diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index 88af1cf6d38bb..e6b8a66b9026f 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -44,7 +44,7 @@ run( 'webpack', '--config', 'x-pack/plugins/canvas/storybook/webpack.dll.config.js', - '--progress', + ...(process.stdout.isTTY && !process.env.CI ? ['--progress'] : []), '--hide-modules', '--display-entrypoints', 'false', diff --git a/x-pack/plugins/cloud/public/mocks.ts b/x-pack/plugins/cloud/public/mocks.ts index 8d9941073140f..52a027e899d0d 100644 --- a/x-pack/plugins/cloud/public/mocks.ts +++ b/x-pack/plugins/cloud/public/mocks.ts @@ -9,8 +9,11 @@ function createSetupMock() { return { cloudId: 'mock-cloud-id', isCloudEnabled: true, - resetPasswordUrl: 'reset-password-url', - accountUrl: 'account-url', + cname: 'cname', + baseUrl: 'base-url', + deploymentUrl: 'deployment-url', + profileUrl: 'profile-url', + organizationUrl: 'organization-url', }; } diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 4c12aa3d92b47..8ca4f7711811a 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -12,12 +12,15 @@ import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { createUserMenuLinks } from './user_menu_links'; +import { getFullCloudUrl } from './utils'; export interface CloudConfigType { id?: string; - resetPasswordUrl?: string; - deploymentUrl?: string; - accountUrl?: string; + cname?: string; + base_url?: string; + profile_url?: string; + deployment_url?: string; + organization_url?: string; } interface CloudSetupDependencies { @@ -30,10 +33,12 @@ interface CloudStartDependencies { export interface CloudSetup { cloudId?: string; - cloudDeploymentUrl?: string; + cname?: string; + baseUrl?: string; + deploymentUrl?: string; + profileUrl?: string; + organizationUrl?: string; isCloudEnabled: boolean; - resetPasswordUrl?: string; - accountUrl?: string; } export class CloudPlugin implements Plugin { @@ -46,33 +51,44 @@ export class CloudPlugin implements Plugin { } public setup(core: CoreSetup, { home }: CloudSetupDependencies) { - const { id, resetPasswordUrl, deploymentUrl } = this.config; + const { + id, + cname, + profile_url: profileUrl, + organization_url: organizationUrl, + deployment_url: deploymentUrl, + base_url: baseUrl, + } = this.config; this.isCloudEnabled = getIsCloudEnabled(id); if (home) { home.environment.update({ cloud: this.isCloudEnabled }); if (this.isCloudEnabled) { - home.tutorials.setVariable('cloud', { id, resetPasswordUrl }); + home.tutorials.setVariable('cloud', { id, baseUrl, profileUrl }); } } return { cloudId: id, - cloudDeploymentUrl: deploymentUrl, + cname, + baseUrl, + deploymentUrl: getFullCloudUrl(baseUrl, deploymentUrl), + profileUrl: getFullCloudUrl(baseUrl, profileUrl), + organizationUrl: getFullCloudUrl(baseUrl, organizationUrl), isCloudEnabled: this.isCloudEnabled, }; } public start(coreStart: CoreStart, { security }: CloudStartDependencies) { - const { deploymentUrl } = this.config; + const { deployment_url: deploymentUrl, base_url: baseUrl } = this.config; coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); - if (deploymentUrl) { + if (baseUrl && deploymentUrl) { coreStart.chrome.setCustomNavLink({ title: i18n.translate('xpack.cloud.deploymentLinkLabel', { defaultMessage: 'Manage this deployment', }), euiIconType: 'arrowLeft', - href: deploymentUrl, + href: getFullCloudUrl(baseUrl, deploymentUrl), }); } diff --git a/x-pack/plugins/cloud/public/user_menu_links.ts b/x-pack/plugins/cloud/public/user_menu_links.ts index e662d51500333..f00911d577c59 100644 --- a/x-pack/plugins/cloud/public/user_menu_links.ts +++ b/x-pack/plugins/cloud/public/user_menu_links.ts @@ -8,30 +8,31 @@ import { i18n } from '@kbn/i18n'; import { UserMenuLink } from '../../security/public'; import { CloudConfigType } from '.'; +import { getFullCloudUrl } from './utils'; export const createUserMenuLinks = (config: CloudConfigType): UserMenuLink[] => { - const { resetPasswordUrl, accountUrl } = config; + const { profile_url: profileUrl, organization_url: organizationUrl, base_url: baseUrl } = config; const userMenuLinks = [] as UserMenuLink[]; - if (resetPasswordUrl) { + if (baseUrl && profileUrl) { userMenuLinks.push({ label: i18n.translate('xpack.cloud.userMenuLinks.profileLinkText', { defaultMessage: 'Profile', }), iconType: 'user', - href: resetPasswordUrl, + href: getFullCloudUrl(baseUrl, profileUrl), order: 100, setAsProfile: true, }); } - if (accountUrl) { + if (baseUrl && organizationUrl) { userMenuLinks.push({ label: i18n.translate('xpack.cloud.userMenuLinks.accountLinkText', { defaultMessage: 'Account & Billing', }), iconType: 'gear', - href: accountUrl, + href: getFullCloudUrl(baseUrl, organizationUrl), order: 200, }); } diff --git a/x-pack/plugins/cloud/public/utils.ts b/x-pack/plugins/cloud/public/utils.ts new file mode 100644 index 0000000000000..e4d4ace64563c --- /dev/null +++ b/x-pack/plugins/cloud/public/utils.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function getFullCloudUrl(baseUrl: string | undefined, dirPath: string | undefined) { + if (baseUrl && dirPath) { + return `${baseUrl}${dirPath}`; + } + + return ''; +} diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index 673df5ac2203b..0e73d59667131 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -22,9 +22,11 @@ const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), id: schema.maybe(schema.string()), apm: schema.maybe(apmConfigSchema), - resetPasswordUrl: schema.maybe(schema.string()), - deploymentUrl: schema.maybe(schema.string()), - accountUrl: schema.maybe(schema.string()), + cname: schema.maybe(schema.string()), + base_url: schema.maybe(schema.string()), + profile_url: schema.maybe(schema.string()), + deployment_url: schema.maybe(schema.string()), + organization_url: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -32,9 +34,11 @@ export type CloudConfigType = TypeOf; export const config: PluginConfigDescriptor = { exposeToBrowser: { id: true, - resetPasswordUrl: true, - deploymentUrl: true, - accountUrl: true, + cname: true, + base_url: true, + profile_url: true, + deployment_url: true, + organization_url: true, }, schema: configSchema, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index d8d66e5ee1998..133f704fd59a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -14,7 +14,7 @@ export const mockKibanaValues = { charts: chartPluginMock.createStartContract(), cloud: { isCloudEnabled: false, - cloudDeploymentUrl: 'https://cloud.elastic.co/deployments/some-id', + deployment_url: 'https://cloud.elastic.co/deployments/some-id', }, history: mockHistory, navigateToUrl: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx index 2140b3392abae..b4108e584086d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx @@ -50,7 +50,7 @@ export const SetupGuideLayout: React.FC = ({ }) => { const { cloud } = useValues(KibanaLogic); const isCloudEnabled = Boolean(cloud.isCloudEnabled); - const cloudDeploymentLink = cloud.cloudDeploymentUrl || ''; + const cloudDeploymentLink = cloud.deploymentUrl || ''; return ( diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index de16f6555d4bd..0eb392e784334 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -13,10 +13,11 @@ import * as AgentService from '../services/agents'; import { isFleetServerSetup } from '../services/fleet_server'; export interface AgentUsage { - total: number; - online: number; - error: number; + total_enrolled: number; + healthy: number; + unhealthy: number; offline: number; + total_all_statuses: number; } export const getAgentUsage = async ( @@ -27,21 +28,26 @@ export const getAgentUsage = async ( // TODO: unsure if this case is possible at all. if (!soClient || !esClient || !(await isFleetServerSetup())) { return { - total: 0, - online: 0, - error: 0, + total_enrolled: 0, + healthy: 0, + unhealthy: 0, offline: 0, + total_all_statuses: 0, }; } - const { total, online, error, offline } = await AgentService.getAgentStatusForAgentPolicy( - soClient, - esClient - ); - return { + const { total, + inactive, online, error, offline, + } = await AgentService.getAgentStatusForAgentPolicy(soClient, esClient); + return { + total_enrolled: total, + healthy: online, + unhealthy: error, + offline, + total_all_statuses: total + inactive, }; }; diff --git a/x-pack/plugins/fleet/server/collectors/register.ts b/x-pack/plugins/fleet/server/collectors/register.ts index 7992d54d1dfad..842bb95fe813f 100644 --- a/x-pack/plugins/fleet/server/collectors/register.ts +++ b/x-pack/plugins/fleet/server/collectors/register.ts @@ -49,10 +49,36 @@ export function registerFleetUsageCollector( schema: { agents_enabled: { type: 'boolean' }, agents: { - total: { type: 'long' }, - online: { type: 'long' }, - error: { type: 'long' }, - offline: { type: 'long' }, + total_enrolled: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents, in any state', + }, + }, + healthy: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents in a healthy state', + }, + }, + unhealthy: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents in an unhealthy state', + }, + }, + offline: { + type: 'long', + _meta: { + description: 'The total number of enrolled agents currently offline', + }, + }, + total_all_statuses: { + type: 'long', + _meta: { + description: 'The total number of agents in any state, both enrolled and inactive', + }, + }, }, packages: { type: 'array', diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index f3fb01655974e..737b6874a8133 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -55,17 +55,18 @@ export async function getAgentStatusForAgentPolicy( agentPolicyId?: string, filterKuery?: string ) { - const [all, online, error, offline, updating] = await pMap( + const [all, allActive, online, error, offline, updating] = await pMap( [ - undefined, + undefined, // All agents, including inactive + undefined, // All active agents AgentStatusKueryHelper.buildKueryForOnlineAgents(), AgentStatusKueryHelper.buildKueryForErrorAgents(), AgentStatusKueryHelper.buildKueryForOfflineAgents(), AgentStatusKueryHelper.buildKueryForUpdatingAgents(), ], - (kuery) => + (kuery, index) => getAgentsByKuery(esClient, { - showInactive: false, + showInactive: index === 0, perPage: 0, page: 1, kuery: joinKuerys( @@ -84,7 +85,8 @@ export async function getAgentStatusForAgentPolicy( return { events: await getEventsCount(soClient, agentPolicyId), - total: all.total, + total: allActive.total, + inactive: all.total - allActive.total, online: online.total, error: error.total, offline: offline.total, diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts index 95072d3ae2e2c..b0dce60085529 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/elastic_index.ts @@ -116,6 +116,11 @@ async function createIndex(esClient: ElasticsearchClient, indexName: string, ind index: indexName, body: { ...indexData, + settings: { + ...(indexData.settings || {}), + auto_expand_replicas: '0-1', + }, + mappings: Object.assign({ ...indexData.mappings, _meta: { ...(indexData.mappings._meta || {}), migrationHash }, diff --git a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json index 3008ee74ab50c..94ad02c6d5f18 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json +++ b/x-pack/plugins/fleet/server/services/fleet_server/elasticsearch/fleet_actions.json @@ -24,6 +24,9 @@ }, "type": { "type": "keyword" + }, + "user_id" : { + "type": "keyword" } } } diff --git a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts index f078b214e4dfd..7af2b791f3707 100644 --- a/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts +++ b/x-pack/plugins/fleet/server/services/fleet_server/saved_object_migrations.ts @@ -25,6 +25,7 @@ import { listEnrollmentApiKeys, getEnrollmentAPIKey } from '../api_keys/enrollme import { appContextService } from '../app_context'; import { isAgentsSetup } from '../agents'; import { agentPolicyService } from '../agent_policy'; +import { invalidateAPIKeys } from '../api_keys'; export async function runFleetServerMigration() { // If Agents are not setup skip as there is nothing to migrate @@ -56,6 +57,7 @@ function getInternalUserSOClient() { async function migrateAgents() { const esClient = appContextService.getInternalUserESClient(); const soClient = getInternalUserSOClient(); + const logger = appContextService.getLogger(); let hasMore = true; while (hasMore) { const res = await soClient.find({ @@ -75,11 +77,20 @@ async function migrateAgents() { .getEncryptedSavedObjects() .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, so.id); + await invalidateAPIKeys( + soClient, + [attributes.access_api_key_id, attributes.default_api_key_id].filter( + (keyId): keyId is string => keyId !== undefined + ) + ).catch((error) => { + logger.error(`Invalidating API keys for agent ${so.id} failed: ${error.message}`); + }); + const body: FleetServerAgent = { type: attributes.type, - active: attributes.active, + active: false, enrolled_at: attributes.enrolled_at, - unenrolled_at: attributes.unenrolled_at, + unenrolled_at: new Date().toISOString(), unenrollment_started_at: attributes.unenrollment_started_at, upgraded_at: attributes.upgraded_at, upgrade_started_at: attributes.upgrade_started_at, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index 2b5319cafb1d4..122fb83edab45 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -247,25 +247,12 @@ export const setup = async (arg?: { }; }; - const createToggleDeletePhaseActions = () => { - const enablePhase = async () => { - await act(async () => { - find('enableDeletePhaseButton').simulate('click'); - }); - component.update(); - }; - - const disablePhase = async () => { - await act(async () => { - find('disableDeletePhaseButton').simulate('click'); - }); - component.update(); - }; - - return { - enablePhase, - disablePhase, - }; + const enableDeletePhase = async (isEnabled: boolean) => { + const buttonSelector = isEnabled ? 'enableDeletePhaseButton' : 'disableDeletePhaseButton'; + await act(async () => { + find(buttonSelector).simulate('click'); + }); + component.update(); }; const hasRolloverSettingRequiredCallout = (): boolean => exists('rolloverSettingsRequired'); @@ -406,7 +393,7 @@ export const setup = async (arg?: { }, delete: { isShown: () => exists('delete-phaseContent'), - ...createToggleDeletePhaseActions(), + enable: enableDeletePhase, ...createMinAgeActions('delete'), }, }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts index 0fb4951e4a4a6..0d49024ac6d67 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts @@ -50,7 +50,7 @@ describe(' delete phase', () => { component.update(); expect(actions.delete.isShown()).toBeFalsy(); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.delete.isShown()).toBeTruthy(); }); @@ -65,7 +65,7 @@ describe(' delete phase', () => { component.update(); expect(actions.delete.hasMinAgeInput()).toBeFalsy(); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.delete.hasMinAgeInput()).toBeTruthy(); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts index 506ac4cece032..ad4d67826b1ed 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts @@ -11,7 +11,7 @@ import { getDefaultHotPhasePolicy } from '../constants'; import { act } from 'react-dom/test-utils'; import { licensingMock } from '../../../../../licensing/public/mocks'; -describe(' timeline', () => { +describe(' rollover', () => { let testBed: EditPolicyTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); @@ -92,7 +92,7 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); await actions.frozen.enable(true); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy(); @@ -108,7 +108,7 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); await actions.frozen.enable(true); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy(); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts index 3618bad45e4f1..a6cce7fdfaca0 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts @@ -55,7 +55,7 @@ describe(' timeline', () => { expect(actions.timeline.hasColdPhase()).toBe(true); expect(actions.timeline.hasDeletePhase()).toBe(false); - await actions.delete.enablePhase(); + await actions.delete.enable(true); expect(actions.timeline.hasHotPhase()).toBe(true); expect(actions.timeline.hasWarmPhase()).toBe(true); expect(actions.timeline.hasColdPhase()).toBe(true); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts index c5c4bb1be87e0..455a7bd442167 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/cold_phase_validation.test.ts @@ -47,38 +47,6 @@ describe(' cold phase validation', () => { ({ runTimers } = testBed); }); - describe('timing', () => { - test(`doesn't allow empty timing`, async () => { - const { actions } = testBed; - - await actions.cold.setMinAgeValue(''); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test(`allows 0 for phase timing`, async () => { - const { actions } = testBed; - - await actions.cold.setMinAgeValue('0'); - - runTimers(); - - actions.expectErrorMessages([]); - }); - - test(`doesn't allow -1 for timing`, async () => { - const { actions } = testBed; - - await actions.cold.setMinAgeValue('-1'); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - }); - describe('replicas', () => { test(`doesn't allow -1 for replicas`, async () => { const { actions } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts deleted file mode 100644 index a13aaa02dcd06..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/delete_phase_validation.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; -import { setupEnvironment } from '../../helpers/setup_environment'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; - -describe(' delete phase validation', () => { - let testBed: EditPolicyTestBed; - let runTimers: () => void; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - server.restore(); - }); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: { data: ['node1'] }, - nodesByAttributes: { 'attribute:true': ['node1'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - httpRequestsMockHelpers.setNodesDetails('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - - await act(async () => { - testBed = await setup(); - }); - - const { component, actions } = testBed; - component.update(); - await actions.setPolicyName('mypolicy'); - await actions.delete.enablePhase(); - - ({ runTimers } = testBed); - }); - - describe('timing', () => { - test(`doesn't allow empty timing`, async () => { - const { actions } = testBed; - - await actions.delete.setMinAgeValue(''); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test(`allows 0 for phase timing`, async () => { - const { actions } = testBed; - - await actions.delete.setMinAgeValue('0'); - - runTimers(); - - actions.expectErrorMessages([]); - }); - - test(`doesn't allow -1 for timing`, async () => { - const { actions } = testBed; - - await actions.delete.setMinAgeValue('-1'); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts index 7c1d687b27e3d..ba260ee33e31e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/hot_phase_validation.test.ts @@ -97,6 +97,17 @@ describe(' hot phase validation', () => { actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); + test(`doesn't allow decimals for max age`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxAge('5.5'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.integerRequired]); + }); + test(`doesn't allow -1 for max docs`, async () => { const { actions } = testBed; @@ -118,6 +129,17 @@ describe(' hot phase validation', () => { actions.expectErrorMessages([i18nTexts.editPolicy.errors.numberGreatThan0Required]); }); + + test(`doesn't allow decimals for max docs`, async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxDocs('5.5'); + + runTimers(); + + actions.expectErrorMessages([i18nTexts.editPolicy.errors.integerRequired]); + }); }); describe('forcemerge', () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts new file mode 100644 index 0000000000000..52009902ab802 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/timing.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { i18nTexts } from '../../../../public/application/sections/edit_policy/i18n_texts'; + +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupEnvironment } from '../../helpers/setup_environment'; + +describe(' timing validation', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] }); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('mypolicy'); + + ({ runTimers } = testBed); + }); + + [ + { + name: `doesn't allow empty timing`, + value: '', + error: [i18nTexts.editPolicy.errors.numberRequired], + }, + { + name: `allows 0 for timing`, + value: '0', + error: [], + }, + { + name: `doesn't allow -1 for timing`, + value: '-1', + error: [i18nTexts.editPolicy.errors.nonNegativeNumberRequired], + }, + { + name: `doesn't allow decimals for timing (with dot)`, + value: '5.5', + error: [i18nTexts.editPolicy.errors.integerRequired], + }, + { + name: `doesn't allow decimals for timing (with comma)`, + value: '5,5', + error: [i18nTexts.editPolicy.errors.integerRequired], + }, + ].forEach((testConfig: { name: string; value: string; error: string[] }) => { + ['warm', 'cold', 'delete', 'frozen'].forEach((phase: string) => { + const { name, value, error } = testConfig; + test(`${phase}: ${name}`, async () => { + const { actions } = testBed; + await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].enable(true); + await actions[phase as 'warm' | 'cold' | 'delete' | 'frozen'].setMinAgeValue(value); + + runTimers(); + + actions.expectErrorMessages(error); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts index 2121dba8e06f6..bfb263e204de9 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/warm_phase_validation.test.ts @@ -47,38 +47,6 @@ describe(' warm phase validation', () => { ({ runTimers } = testBed); }); - describe('timing', () => { - test(`doesn't allow empty timing`, async () => { - const { actions } = testBed; - - await actions.warm.setMinAgeValue(''); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - - test(`allows 0 for phase timing`, async () => { - const { actions } = testBed; - - await actions.warm.setMinAgeValue('0'); - - runTimers(); - - actions.expectErrorMessages([]); - }); - - test(`doesn't allow -1 for timing`, async () => { - const { actions } = testBed; - - await actions.warm.setMinAgeValue('-1'); - - runTimers(); - - actions.expectErrorMessages([i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); - }); - }); - describe('replicas', () => { test(`doesn't allow -1 for replicas`, async () => { const { actions } = testBed; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts index 61ceab1990c72..17dadb1c6b47e 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -13,6 +13,7 @@ import { POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, } from '../constants'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { licensingMock } from '../../../../../licensing/public/mocks'; describe(' serialization', () => { let testBed: EditPolicyTestBed; @@ -58,7 +59,7 @@ describe(' serialization', () => { // Set max docs to test whether we keep the unknown fields in that object after serializing await actions.hot.setMaxDocs('1000'); // Remove the delete phase to ensure that we also correctly remove data - await actions.delete.disablePhase(); + await actions.delete.enable(false); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -91,6 +92,78 @@ describe(' serialization', () => { }, }); }); + + it('default policy (only policy name input) on enterprise license', async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('test_policy'); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + name: 'test_policy', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + min_age: '0ms', + }, + }, + }); + }); + + it('default policy (only policy name input) on basic license', async () => { + httpRequestsMockHelpers.setLoadPolicies([]); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component, actions } = testBed; + component.update(); + await actions.setPolicyName('test_policy'); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + name: 'test_policy', + phases: { + hot: { + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, + }, + }, + min_age: '0ms', + }, + }, + }); + }); }); describe('hot phase', () => { @@ -408,7 +481,7 @@ describe(' serialization', () => { test('delete phase', async () => { const { actions } = testBed; - await actions.delete.enablePhase(); + await actions.delete.enable(true); await actions.setWaitForSnapshotPolicy('test'); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index ffd4e2758ab86..8c90a738d2c09 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -60,7 +60,7 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); const isCloudEnabled = cloud?.isCloudEnabled ?? false; - const cloudDeploymentUrl = cloud?.cloudDeploymentUrl; + const cloudDeploymentUrl = cloud?.deploymentUrl; const renderNotice = () => { switch (allocationType) { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index c0e489042586c..2b90d75fa6da0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -15,7 +15,7 @@ import { ifExistsNumberGreaterThanZero, ifExistsNumberNonNegative, rolloverThresholdsValidator, - minAgeValidator, + integerValidator, } from './validations'; const rolloverFormPaths = Object.values(ROLLOVER_FORM_PATHS); @@ -117,6 +117,20 @@ const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ serializer: serializers.stringToNumber, }); +const getMinAgeField = (defaultValue: string = '0') => ({ + defaultValue, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: ifExistsNumberNonNegative, + }, + { + validator: integerValidator, + }, + ], +}); export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ _meta: { hot: { @@ -254,6 +268,9 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ { validator: ifExistsNumberGreaterThanZero, }, + { + validator: integerValidator, + }, ], fieldsToValidateOnChange: rolloverFormPaths, }, @@ -268,6 +285,9 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ { validator: ifExistsNumberGreaterThanZero, }, + { + validator: integerValidator, + }, ], serializer: serializers.stringToNumber, fieldsToValidateOnChange: rolloverFormPaths, @@ -300,14 +320,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, warm: { - min_age: { - defaultValue: '0', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField(), actions: { allocate: { number_of_replicas: numberOfReplicasField, @@ -324,14 +337,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, cold: { - min_age: { - defaultValue: '0', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField(), actions: { allocate: { number_of_replicas: numberOfReplicasField, @@ -343,14 +349,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, frozen: { - min_age: { - defaultValue: '0', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField(), actions: { allocate: { number_of_replicas: numberOfReplicasField, @@ -362,14 +361,7 @@ export const getSchema = (isCloudEnabled: boolean): FormSchema => ({ }, }, delete: { - min_age: { - defaultValue: '365', - validations: [ - { - validator: minAgeValidator, - }, - ], - }, + min_age: getMinAgeField('365'), actions: { wait_for_snapshot: { policy: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 57112b0e1cb16..b10e3294f75c7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -254,7 +254,7 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( /** * FROZEN PHASE SERIALIZATION */ - if (_meta.frozen.enabled) { + if (_meta.frozen?.enabled) { draft.phases.frozen!.actions = draft.phases.frozen?.actions ?? {}; const frozenPhase = draft.phases.frozen!; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts index 5be49d25aed7f..ce85913d5db74 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/validations.ts @@ -89,15 +89,11 @@ export const rolloverThresholdsValidator: ValidationFunc = ({ form, path }) => { } }; -export const minAgeValidator: ValidationFunc = (arg) => - numberGreaterThanField({ - than: 0, - allowEquality: true, - message: i18nTexts.editPolicy.errors.nonNegativeNumberRequired, - })({ - ...arg, - value: arg.value === '' ? -Infinity : parseInt(arg.value, 10), - }); +export const integerValidator: ValidationFunc = (arg) => { + if (!Number.isInteger(Number(arg.value ?? ''))) { + return { message: i18nTexts.editPolicy.errors.integerRequired }; + } +}; export const createPolicyNameValidations = ({ policies, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 47585fba38768..d9bd9b664d205 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -102,6 +102,12 @@ export const i18nTexts = { defaultMessage: 'Only numbers above 0 are allowed.', } ), + integerRequired: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.errors.integerRequiredError', + { + defaultMessage: 'Only integers are allowed.', + } + ), maximumAgeRequiredMessage: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.errors.maximumAgeMissingError', { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx index a210831eef865..f6d739078002e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -24,6 +24,7 @@ import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; import { DEFAULT_K8S_PARTITION_FIELD } from '../../../../../../containers/ml/modules/metrics_k8s/module_descriptor'; import { MetricsExplorerKueryBar } from '../../../../metrics_explorer/components/kuery_bar'; import { convertKueryToElasticSearchQuery } from '../../../../../../utils/kuery'; +import { useUiTracker } from '../../../../../../../../observability/public'; interface Props { jobType: 'hosts' | 'kubernetes'; @@ -40,6 +41,7 @@ export const JobSetupScreen = (props: Props) => { const k = useMetricK8sModuleContext(); const [filter, setFilter] = useState(''); const [filterQuery, setFilterQuery] = useState(''); + const trackMetric = useUiTracker({ app: 'infra_metrics' }); const { createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', }); @@ -137,9 +139,25 @@ export const JobSetupScreen = (props: Props) => { useEffect(() => { if (setupStatus.type === 'succeeded') { + if (props.jobType === 'kubernetes') { + trackMetric({ metric: 'metrics_ml_anomaly_detection_k8s_enabled' }); + if ( + partitionField && + (partitionField.length !== 1 || partitionField[0] !== DEFAULT_K8S_PARTITION_FIELD) + ) { + trackMetric({ metric: 'metrics_ml_anomaly_detection_k8s_partition_changed' }); + } + } else { + trackMetric({ metric: 'metrics_ml_anomaly_detection_hosts_enabled' }); + if (partitionField) { + trackMetric({ metric: 'metrics_ml_anomaly_detection_hosts_partition_changed' }); + } + trackMetric({ metric: 'metrics_ml_anomaly_detection_hosts_enabled' }); + } + goHome(); } - }, [setupStatus, goHome]); + }, [setupStatus, props.jobType, partitionField, trackMetric, goHome]); return ( <> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss index 55630fa96d9b0..d51dee31e9f8e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss @@ -17,11 +17,6 @@ box-shadow: none; } - // Remove the box-shadow on all nested items - .pipelineProcessorsEditor__item { - box-shadow: none !important; - } - &__processorTypeLabel { line-height: $euiButtonHeightSmall; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx index 27035a8d269f2..320f99acd08f7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -158,7 +158,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( }; return ( - + { if (!state.activeData) { @@ -577,7 +585,7 @@ export function App({ } }, saveAndReturn: () => { - if (savingPermitted && lastKnownDoc) { + if (savingToDashboardPermitted && lastKnownDoc) { // disabling the validation on app leave because the document has been saved. onAppLeave((actions) => { return actions.default(); @@ -597,7 +605,7 @@ export function App({ } }, showSaveModal: () => { - if (savingPermitted) { + if (savingToDashboardPermitted || savingToLibraryPermitted) { setState((s) => ({ ...s, isSaveModalVisible: true })); } }, @@ -697,6 +705,7 @@ export function App({ { const { originatingApp, + savingToLibraryPermitted, savedObjectsTagging, tagsIds, lastKnownDoc, @@ -85,13 +87,15 @@ export const SaveModal = (props: Props) => { { const saveToLibrary = Boolean(saveProps.addToLibrary); onSave(saveProps, { saveToLibrary }); }} onClose={onClose} documentInfo={{ - id: lastKnownDoc.savedObjectId, + // if the user cannot save to the library - treat this as a new document. + id: savingToLibraryPermitted ? lastKnownDoc.savedObjectId : undefined, title: lastKnownDoc.title || '', description: lastKnownDoc.description || '', }} diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 157975b630e1e..00eaadeaf8299 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -112,7 +112,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -151,7 +154,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -191,7 +194,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -231,7 +237,10 @@ describe('embeddable', () => { indexPatternService: ({ get: (id: string) => Promise.resolve({ id }), } as unknown) as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -266,7 +275,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -307,7 +319,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -352,7 +364,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -395,7 +410,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -445,7 +463,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -495,7 +516,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -544,7 +568,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: ({ get: jest.fn() } as unknown) as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -582,7 +609,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -620,7 +650,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -658,7 +691,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -711,7 +747,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -780,7 +819,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -824,7 +866,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ @@ -868,7 +913,10 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - editable: true, + capabilities: { + canSaveDashboards: true, + canSaveVisualizations: true, + }, getTrigger, documentToExpression: () => Promise.resolve({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 1db067606dc82..a3316e0083d35 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -88,13 +88,13 @@ export interface LensEmbeddableDeps { documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; - editable: boolean; indexPatternService: IndexPatternsContract; expressionRenderer: ReactExpressionRendererType; timefilter: TimefilterContract; basePath: IBasePath; getTrigger?: UiActionsStart['getTrigger'] | undefined; getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; + capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean }; } export class Embeddable @@ -129,7 +129,6 @@ export class Embeddable initialInput, { editApp: 'lens', - editable: deps.editable, }, parent ); @@ -326,7 +325,7 @@ export class Embeddable hasCompatibleActions={this.hasCompatibleActions} className={input.className} style={input.style} - canEdit={this.deps.editable && input.viewMode === 'edit'} + canEdit={this.getIsEditable() && input.viewMode === 'edit'} />, domNode ); @@ -451,6 +450,7 @@ export class Embeddable this.updateOutput({ ...this.getOutput(), defaultTitle: this.savedVis.title, + editable: this.getIsEditable(), title, editPath: getEditPath(savedObjectId), editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`), @@ -458,6 +458,13 @@ export class Embeddable }); } + private getIsEditable() { + return ( + this.deps.capabilities.canSaveVisualizations || + (!this.inputIsRefType(this.getInput()) && this.deps.capabilities.canSaveDashboards) + ); + } + public inputIsRefType = ( input: LensByValueInput | LensByReferenceInput ): input is LensByReferenceInput => { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index a676b7283671c..1a4962bd1fe8e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -53,7 +53,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { public isEditable = async () => { const { capabilities } = await this.getStartServices(); - return capabilities.visualize.save as boolean; + return Boolean(capabilities.visualize.save || capabilities.dashboard?.showWriteControls); }; canCreateNew() { @@ -86,6 +86,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { coreHttp, attributeService, indexPatternService, + capabilities, } = await this.getStartServices(); const { Embeddable } = await import('../../async_services'); @@ -96,11 +97,14 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, timefilter, expressionRenderer, - editable: await this.isEditable(), basePath: coreHttp.basePath, getTrigger: uiActions?.getTrigger, getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions, documentToExpression, + capabilities: { + canSaveDashboards: Boolean(capabilities.dashboard?.showWriteControls), + canSaveVisualizations: Boolean(capabilities.visualize.save), + }, }, input, parent diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 14834adfc33cc..0ea533e22e4d9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -474,53 +474,6 @@ describe('IndexPattern Data Source', () => { expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); }); - it('should add the suffix to the remap column id if provided by the operation', async () => { - const queryBaseState: IndexPatternBaseState = { - currentIndexPatternId: '1', - layers: { - first: { - indexPatternId: '1', - columnOrder: ['def', 'abc'], - columns: { - abc: { - label: '23rd percentile', - dataType: 'number', - isBucketed: false, - sourceField: 'bytes', - operationType: 'percentile', - params: { - percentile: 23, - }, - }, - def: { - label: 'Terms', - dataType: 'string', - isBucketed: true, - operationType: 'terms', - sourceField: 'source', - params: { - size: 5, - orderBy: { - type: 'alphabetical', - }, - orderDirection: 'asc', - }, - }, - }, - }, - }, - }; - - const state = enrichBaseState(queryBaseState); - - const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; - expect(Object.keys(JSON.parse(ast.chain[1].arguments.idMap[0] as string))).toEqual([ - 'col-0-def', - // col-1 is the auto naming of esasggs, abc is the specified column id, .23 is the generated suffix - 'col-1-abc.23', - ]); - }); - it('should wrap filtered metrics in filtered metric aggregation', async () => { const queryBaseState: IndexPatternBaseState = { currentIndexPatternId: '1', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx index 2d2227396afa6..b7e92a0b54952 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/helpers.tsx @@ -64,13 +64,6 @@ export function getInvalidFieldMessage( : undefined; } -export function getEsAggsSuffix(column: IndexPatternColumn) { - const operationDefinition = operationDefinitionMap[column.operationType]; - return operationDefinition.input === 'field' && operationDefinition.getEsAggsSuffix - ? operationDefinition.getEsAggsSuffix(column) - : ''; -} - export function getSafeName(name: string, indexPattern: IndexPattern): string { const field = indexPattern.getFieldByName(name); return field diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index b3aa93b062eb1..0b63dc6ece974 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -311,13 +311,6 @@ interface FieldBasedOperationDefinition { layer: IndexPatternLayer, uiSettings: IUiSettingsClient ) => ExpressionAstFunction; - /** - * Optional function to return the suffix used for ES bucket paths and esaggs column id. - * This is relevant for multi metrics to pick the right value. - * - * @param column The current column - */ - getEsAggsSuffix?: (column: C) => string; /** * Validate that the operation has the right preconditions in the state. For example: * diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 9ac91be5a17ec..c14ff9f86f602 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -127,7 +127,7 @@ describe('percentile', () => { expect(esAggsFn).toEqual( expect.objectContaining({ arguments: expect.objectContaining({ - percents: [23], + percentile: [23], field: ['a'], }), }) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx index 639b9e3a95c47..dd0f3b978da5f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.tsx @@ -51,6 +51,7 @@ export const percentileOperation: OperationDefinition { if (supportedFieldTypes.includes(fieldType) && aggregatable && !aggregationRestrictions) { return { @@ -86,6 +87,7 @@ export const percentileOperation: OperationDefinition { - return buildExpressionFunction('aggPercentiles', { - id: columnId, - enabled: true, - schema: 'metric', - field: column.sourceField, - percents: [column.params.percentile], - }).toAst(); - }, - getEsAggsSuffix: (column) => { - const value = column.params.percentile; - return `.${value}`; + return buildExpressionFunction( + 'aggSinglePercentile', + { + id: columnId, + enabled: true, + schema: 'metric', + field: column.sourceField, + percentile: column.params.percentile, + } + ).toAst(); }, getErrorMessage: (layer, columnId, indexPattern) => getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index a4a061db04797..857e8b3605cfc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -23,7 +23,7 @@ import { DataType } from '../../../../types'; import { OperationDefinition } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesInput } from './values_input'; -import { getEsAggsSuffix, getInvalidFieldMessage } from '../helpers'; +import { getInvalidFieldMessage } from '../helpers'; import type { IndexPatternLayer } from '../../../types'; function ofName(name?: string) { @@ -137,11 +137,7 @@ export const termsOperation: OperationDefinition { }) ); }); - - it('should include esaggs suffix from other columns in orderby argument', () => { - const termsColumn = layer.columns.col1 as TermsIndexPatternColumn; - const esAggsFn = termsOperation.toEsAggsFn( - { - ...termsColumn, - params: { - ...termsColumn.params, - otherBucket: true, - orderBy: { type: 'column', columnId: 'abcde' }, - }, - }, - 'col1', - {} as IndexPattern, - { - ...layer, - columns: { - ...layer.columns, - abcde: { - dataType: 'number', - isBucketed: false, - operationType: 'percentile', - sourceField: 'abc', - label: '', - params: { - percentile: 12, - }, - }, - }, - }, - uiSettingsMock - ); - expect(esAggsFn).toEqual( - expect.objectContaining({ - arguments: expect.objectContaining({ - orderBy: ['abcde.12'], - }), - }) - ); - }); }); describe('onFieldChange', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts index 2e4de8b52dacb..34579927cfe19 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.test.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { Datatable } from 'src/plugins/expressions/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataPublicPluginStart, TimeRange } from 'src/plugins/data/public'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils'; import { getTimeScaleFunction, TimeScaleArgs } from './time_scale'; @@ -44,9 +44,25 @@ describe('time_scale', () => { targetUnit: 'h', }; + function setDateHistogramMeta(options: { + timeZone: string; + timeRange: TimeRange; + interval: string; + }) { + emptyTable.columns[0].meta.source = 'esaggs'; + emptyTable.columns[0].meta.sourceParams = { + type: 'date_histogram', + params: { + used_interval: options.interval, + used_time_zone: options.timeZone, + }, + appliedTimeRange: options.timeRange, + }; + } + beforeEach(() => { dataMock = dataPluginMock.createStartContract(); - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T00:00:00.000Z', @@ -156,7 +172,7 @@ describe('time_scale', () => { }); it('should be able to scale up as well', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T12:00:00.000Z', @@ -196,7 +212,7 @@ describe('time_scale', () => { }); it('can scale starting from unit multiple target intervals', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T13:00:00.000Z', @@ -238,7 +254,7 @@ describe('time_scale', () => { }); it('take start and end of timerange into account', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2020-10-05T12:00:00.000Z', @@ -283,7 +299,7 @@ describe('time_scale', () => { }); it('should respect DST switches', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'Europe/Berlin', timeRange: { from: '2020-10-23T00:00:00.000+02:00', @@ -323,7 +339,7 @@ describe('time_scale', () => { }); it('take leap years into account', async () => { - (dataMock.search.aggs.getDateMetaByDatatableColumn as jest.Mock).mockReturnValue({ + setDateHistogramMeta({ timeZone: 'UTC', timeRange: { from: '2010-01-01T00:00:00.000Z', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts index 33395de389125..368e06110efc9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -97,8 +97,8 @@ export function getTimeScaleFunction(data: DataPublicPluginStart) { } const targetUnitInMs = unitInMs[targetUnit]; - const timeInfo = await data.search.aggs.getDateMetaByDatatableColumn(dateColumnDefinition); - const intervalDuration = timeInfo && search.aggs.parseInterval(timeInfo.interval); + const timeInfo = search.aggs.getDateHistogramMetaDataByDatatableColumn(dateColumnDefinition); + const intervalDuration = timeInfo?.interval && search.aggs.parseInterval(timeInfo.interval); if (!timeInfo || !intervalDuration) { throw new Error( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index d786d781199b6..b272e5476aa63 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -23,7 +23,6 @@ import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types'; import { OriginalColumn } from './rename_columns'; import { dateHistogramOperation } from './operations/definitions'; -import { getEsAggsSuffix } from './operations/definitions/helpers'; function getExpressionForLayer( layer: IndexPatternLayer, @@ -104,10 +103,9 @@ function getExpressionForLayer( const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => { const esAggsId = `col-${columnEntries.length === 1 ? 0 : index}-${colId}`; - const suffix = getEsAggsSuffix(column); return { ...currentIdMap, - [`${esAggsId}${suffix}`]: { + [esAggsId]: { ...column, id: colId, }, diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index c3ce391f4469d..2487ddf32cd1f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -1894,10 +1894,14 @@ describe('xy_expression', () => { xyProps.args.layers[0].xScaleType = 'time'; }); it('should use first valid layer and determine interval', async () => { - const result = await calculateMinInterval( - xyProps, - jest.fn().mockResolvedValue({ interval: '5m' }) - ); + xyProps.data.tables.first.columns[2].meta.source = 'esaggs'; + xyProps.data.tables.first.columns[2].meta.sourceParams = { + type: 'date_histogram', + params: { + used_interval: '5m', + }, + }; + const result = await calculateMinInterval(xyProps); expect(result).toEqual(5 * 60 * 1000); }); @@ -1915,34 +1919,38 @@ describe('xy_expression', () => { }, }, }; - const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + const result = await calculateMinInterval(xyProps); expect(result).toEqual(5); }); it('should return undefined if data table is empty', async () => { xyProps.data.tables.first.rows = []; - const result = await calculateMinInterval( - xyProps, - jest.fn().mockResolvedValue({ interval: '5m' }) - ); + xyProps.data.tables.first.columns[2].meta.source = 'esaggs'; + xyProps.data.tables.first.columns[2].meta.sourceParams = { + type: 'date_histogram', + params: { + used_interval: '5m', + }, + }; + const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); }); it('should return undefined if interval can not be checked', async () => { - const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); }); it('should return undefined if date column is not found', async () => { xyProps.data.tables.first.columns.splice(2, 1); - const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); }); it('should return undefined if x axis is not a date', async () => { xyProps.args.layers[0].xScaleType = 'ordinal'; xyProps.data.tables.first.columns.splice(2, 1); - const result = await calculateMinInterval(xyProps, jest.fn().mockResolvedValue(undefined)); + const result = await calculateMinInterval(xyProps); expect(result).toEqual(undefined); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 5615a9ac34898..d90d092b65c57 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -46,11 +46,7 @@ import { import { XYArgs, SeriesType, visualizationTypes, LayerArgs } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart, getSeriesColor } from './state_helpers'; -import { - DataPublicPluginStart, - ExpressionValueSearchContext, - search, -} from '../../../../../src/plugins/data/public'; +import { ExpressionValueSearchContext, search } from '../../../../../src/plugins/data/public'; import { ChartsPluginSetup, PaletteRegistry, @@ -200,10 +196,7 @@ export const xyChart: ExpressionFunctionDefinition< }, }; -export async function calculateMinInterval( - { args: { layers }, data }: XYChartProps, - getIntervalByColumn: DataPublicPluginStart['search']['aggs']['getDateMetaByDatatableColumn'] -) { +export async function calculateMinInterval({ args: { layers }, data }: XYChartProps) { const filteredLayers = getFilteredLayers(layers, data); if (filteredLayers.length === 0) return; const isTimeViz = data.dateRange && filteredLayers.every((l) => l.xScaleType === 'time'); @@ -220,9 +213,9 @@ export async function calculateMinInterval( return undefined; } } - const dateMetaData = await getIntervalByColumn(xColumn); - if (!dateMetaData) return; - const intervalDuration = search.aggs.parseInterval(dateMetaData.interval); + const dateInterval = search.aggs.getDateHistogramMetaDataByDatatableColumn(xColumn)?.interval; + if (!dateInterval) return; + const intervalDuration = search.aggs.parseInterval(dateInterval); if (!intervalDuration) return; return intervalDuration.as('milliseconds'); } @@ -231,7 +224,6 @@ export const getXyChartRenderer = (dependencies: { formatFactory: Promise; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; - getIntervalByColumn: DataPublicPluginStart['search']['aggs']['getDateMetaByDatatableColumn']; timeZone: string; }): ExpressionRenderDefinition => ({ name: 'lens_xy_chart_renderer', @@ -262,7 +254,7 @@ export const getXyChartRenderer = (dependencies: { chartsThemeService={dependencies.chartsThemeService} paletteService={dependencies.paletteService} timeZone={dependencies.timeZone} - minInterval={await calculateMinInterval(config, dependencies.getIntervalByColumn)} + minInterval={await calculateMinInterval(config)} onClickValue={onClickValue} onSelectRange={onSelectRange} renderMode={handlers.getRenderMode()} diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 1ca0bbb3f2da9..8650dedca0e2c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -63,7 +63,6 @@ export class XyVisualization { chartsThemeService: charts.theme, paletteService: palettes, timeZone: getTimeZone(core.uiSettings), - getIntervalByColumn: data.search.aggs.getDateMetaByDatatableColumn, }) ); return getXyVisualization({ paletteService: palettes, data }); diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts index ef2c9ac807c1d..f9c0e324aa5d8 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.ts @@ -14,8 +14,6 @@ import { trackMapSettings } from './map_actions'; import { setSelectedLayer } from './layer_actions'; export const UPDATE_FLYOUT = 'UPDATE_FLYOUT'; -export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW'; -export const OPEN_SET_VIEW = 'OPEN_SET_VIEW'; export const SET_IS_LAYER_TOC_OPEN = 'SET_IS_LAYER_TOC_OPEN'; export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; export const SET_READ_ONLY = 'SET_READ_ONLY'; @@ -50,16 +48,6 @@ export function openMapSettings() { dispatch(updateFlyout(FLYOUT_STATE.MAP_SETTINGS_PANEL)); }; } -export function closeSetView() { - return { - type: CLOSE_SET_VIEW, - }; -} -export function openSetView() { - return { - type: OPEN_SET_VIEW, - }; -} export function setIsLayerTOCOpen(isLayerTOCOpen: boolean) { return { type: SET_IS_LAYER_TOC_OPEN, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx index 5c2e11813bb5f..47c2012d6ed8f 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/data_mapping/data_mapping_popover.tsx @@ -25,9 +25,9 @@ export class DataMappingPopover extends Component { }; _togglePopover = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); }; _closePopover = () => { diff --git a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts index 5f81a74ab03ce..a8bc5b9a821f0 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { Filter } from '../../../../../../src/plugins/data/public'; -import { TooltipFeature } from '../../../../../plugins/maps/common/descriptor_types'; +import type { TooltipFeature } from '../../../../../plugins/maps/common/descriptor_types'; export interface ITooltipProperty { getPropertyKey(): string; diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx index 78739731e14b6..04ae7af62fddc 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/add_tooltip_field_popover.tsx @@ -98,9 +98,9 @@ export class AddTooltipFieldPopover extends Component { } _togglePopover = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); }; _closePopover = () => { diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 622aeae3cbb87..525ba394ed503 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -16,7 +16,6 @@ import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { MBMap } from '../mb_map'; // @ts-expect-error import { WidgetOverlay } from '../widget_overlay'; -// @ts-expect-error import { ToolbarOverlay } from '../toolbar_overlay'; // @ts-expect-error import { LayerPanel } from '../layer_panel'; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap index 3407bcfd4f845..506767fcd4706 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap @@ -35,7 +35,12 @@ exports[`Must zoom tools and draw filter tools 1`] = ` diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js deleted file mode 100644 index 6470718fc7e4a..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { connect } from 'react-redux'; -import { ToolbarOverlay } from './toolbar_overlay'; - -function mapStateToProps() { - return {}; -} - -const connectedToolbarOverlay = connect(mapStateToProps, null)(ToolbarOverlay); -export { connectedToolbarOverlay as ToolbarOverlay }; diff --git a/x-pack/plugins/banners/public/types.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts similarity index 68% rename from x-pack/plugins/banners/public/types.ts rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts index 1f0ce524a785e..d1008edfd572d 100644 --- a/x-pack/plugins/banners/public/types.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -import { BannerPlacement } from '../common'; - -export interface BannerClientConfig { - placement: BannerPlacement; -} +export { ToolbarOverlay } from './toolbar_overlay'; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.ts similarity index 62% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.ts index 3220f84967f16..8f7a3cf762a6b 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.ts @@ -5,33 +5,27 @@ * 2.0. */ +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { SetViewControl } from './set_view_control'; -import { setGotoWithCenter, closeSetView, openSetView } from '../../../actions'; +import { setGotoWithCenter } from '../../../actions'; import { getMapZoom, getMapCenter, getMapSettings } from '../../../selectors/map_selectors'; -import { getIsSetViewOpen } from '../../../selectors/ui_selectors'; +import { MapStoreState } from '../../../reducers/store'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { return { settings: getMapSettings(state), - isSetViewOpen: getIsSetViewOpen(state), zoom: getMapZoom(state), center: getMapCenter(state), }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { - onSubmit: ({ lat, lon, zoom }) => { - dispatch(closeSetView()); + onSubmit: ({ lat, lon, zoom }: { lat: number; lon: number; zoom: number }) => { dispatch(setGotoWithCenter({ lat, lon, zoom })); }, - closeSetView: () => { - dispatch(closeSetView()); - }, - openSetView: () => { - dispatch(openSetView()); - }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx similarity index 69% rename from x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx index 21818476d6965..b657d6369f8aa 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { ChangeEvent, Component } from 'react'; import { EuiForm, EuiFormRow, @@ -19,57 +18,86 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; - -function getViewString(lat, lon, zoom) { - return `${lat},${lon},${zoom}`; +import { MapCenter } from '../../../../common/descriptor_types'; +import { MapSettings } from '../../../reducers/map'; + +export interface Props { + settings: MapSettings; + zoom: number; + center: MapCenter; + onSubmit: ({ lat, lon, zoom }: { lat: number; lon: number; zoom: number }) => void; } -export class SetViewControl extends Component { - state = {}; - - static getDerivedStateFromProps(nextProps, prevState) { - const nextView = getViewString(nextProps.center.lat, nextProps.center.lon, nextProps.zoom); - if (nextView !== prevState.prevView) { - return { - lat: nextProps.center.lat, - lon: nextProps.center.lon, - zoom: nextProps.zoom, - prevView: nextView, - }; - } +interface State { + isPopoverOpen: boolean; + lat: number | string; + lon: number | string; + zoom: number | string; +} - return null; - } +export class SetViewControl extends Component { + state: State = { + isPopoverOpen: false, + lat: 0, + lon: 0, + zoom: 0, + }; _togglePopover = () => { - if (this.props.isSetViewOpen) { - this.props.closeSetView(); + if (this.state.isPopoverOpen) { + this._closePopover(); return; } - this.props.openSetView(); + this.setState({ + lat: this.props.center.lat, + lon: this.props.center.lon, + zoom: this.props.zoom, + isPopoverOpen: true, + }); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); }; - _onLatChange = (evt) => { + _onLatChange = (evt: ChangeEvent) => { this._onChange('lat', evt); }; - _onLonChange = (evt) => { + _onLonChange = (evt: ChangeEvent) => { this._onChange('lon', evt); }; - _onZoomChange = (evt) => { + _onZoomChange = (evt: ChangeEvent) => { this._onChange('zoom', evt); }; - _onChange = (name, evt) => { + _onChange = (name: 'lat' | 'lon' | 'zoom', evt: ChangeEvent) => { const sanitizedValue = parseFloat(evt.target.value); + // @ts-expect-error this.setState({ [name]: isNaN(sanitizedValue) ? '' : sanitizedValue, }); }; - _renderNumberFormRow = ({ value, min, max, onChange, label, dataTestSubj }) => { + _renderNumberFormRow = ({ + value, + min, + max, + onChange, + label, + dataTestSubj, + }: { + value: string | number; + min: number; + max: number; + onChange: (evt: ChangeEvent) => void; + label: string; + dataTestSubj: string; + }) => { const isInvalid = value === '' || value > max || value < min; const error = isInvalid ? `Must be between ${min} and ${max}` : null; return { @@ -90,7 +118,8 @@ export class SetViewControl extends Component { _onSubmit = () => { const { lat, lon, zoom } = this.state; - this.props.onSubmit({ lat, lon, zoom }); + this._closePopover(); + this.props.onSubmit({ lat: lat as number, lon: lon as number, zoom: zoom as number }); }; _renderSetViewForm() { @@ -175,23 +204,11 @@ export class SetViewControl extends Component { })} /> } - isOpen={this.props.isSetViewOpen} - closePopover={this.props.closeSetView} + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} > {this._renderSetViewForm()} ); } } - -SetViewControl.propTypes = { - isSetViewOpen: PropTypes.bool.isRequired, - zoom: PropTypes.number.isRequired, - center: PropTypes.shape({ - lat: PropTypes.number.isRequired, - lon: PropTypes.number.isRequired, - }), - onSubmit: PropTypes.func.isRequired, - closeSetView: PropTypes.func.isRequired, - openSetView: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js deleted file mode 100644 index ceca3f5b7fdc1..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { SetViewControl } from './set_view_control'; -import { ToolsControl } from './tools_control'; -import { FitToData } from './fit_to_data'; - -export class ToolbarOverlay extends React.Component { - _renderToolsControl() { - const { addFilters, geoFields, getFilterActions, getActionContext } = this.props; - if (!addFilters || !geoFields.length) { - return null; - } - - return ( - - - - ); - } - - render() { - return ( - - - - - - - - - - {this._renderToolsControl()} - - ); - } -} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx index a6d17819e2fea..d8ac971ae3983 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { Filter } from 'src/plugins/data/public'; jest.mock('../../kibana_services', () => { return { @@ -16,15 +17,25 @@ jest.mock('../../kibana_services', () => { }; }); -// @ts-ignore import { ToolbarOverlay } from './toolbar_overlay'; test('Must render zoom tools', async () => { - const component = shallow(); + const component = shallow(); expect(component).toMatchSnapshot(); }); test('Must zoom tools and draw filter tools', async () => { - const component = shallow( {}} geoFields={['coordinates']} />); + const geoFieldWithIndex = { + geoFieldName: 'myGeoFieldName', + geoFieldType: 'geo_point', + indexPatternTitle: 'myIndex', + indexPatternId: '1', + }; + const component = shallow( + {}} + geoFields={[geoFieldWithIndex]} + /> + ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx new file mode 100644 index 0000000000000..c5208bc254fc8 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Filter } from 'src/plugins/data/public'; +import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; +import { SetViewControl } from './set_view_control'; +import { ToolsControl } from './tools_control'; +import { FitToData } from './fit_to_data'; +import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; + +export interface Props { + addFilters?: ((filters: Filter[], actionId: string) => Promise) | null; + geoFields: GeoFieldWithIndex[]; + getFilterActions?: () => Promise; + getActionContext?: () => ActionExecutionContext; +} + +export function ToolbarOverlay(props: Props) { + function renderToolsControl() { + const { addFilters, geoFields, getFilterActions, getActionContext } = props; + if (!addFilters || !geoFields.length) { + return null; + } + + return ( + + + + ); + } + + return ( + + + + + + + + + + {renderToolsControl()} + + ); +} diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index b1944f8136709..a4ce76b702d13 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -11,7 +11,6 @@ import { EmbeddableFactoryDefinition, IContainer, } from '../../../../../src/plugins/embeddable/public'; -import '../index.scss'; import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types'; diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index 3e6cd8d14ad37..dc9cb2d594fe3 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -19,6 +19,8 @@ export const plugin: PluginInitializer = ( export { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; -export { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; +export type { RenderTooltipContentParams } from './classes/tooltips/tooltip_property'; export { MapsStartApi } from './api'; + +export type { MapEmbeddable, MapEmbeddableInput } from './embeddable'; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 6fd14d8d42e18..e4b9397fab8e7 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -5,13 +5,12 @@ * 2.0. */ -import _ from 'lodash'; -import { CoreStart } from 'kibana/public'; +import type { CoreStart } from 'kibana/public'; import type { MapsEmsConfig } from '../../../../src/plugins/maps_ems/public'; -import { MapsConfigType } from '../config'; -import { MapsPluginStartDependencies } from './plugin'; -import { EMSSettings } from '../common/ems_settings'; -import { PaletteRegistry } from '../../../../src/plugins/charts/public'; +import type { MapsConfigType } from '../config'; +import type { MapsPluginStartDependencies } from './plugin'; +import type { EMSSettings } from '../common/ems_settings'; +import type { PaletteRegistry } from '../../../../src/plugins/charts/public'; let kibanaVersion: string; export const setKibanaVersion = (version: string) => (kibanaVersion = version); @@ -75,8 +74,22 @@ export const getEMSSettings = () => { export const getEmsTileLayerId = () => getKibanaCommonConfig().emsTileLayerId; -export const getRegionmapLayers = () => _.get(getKibanaCommonConfig(), 'regionmap.layers', []); -export const getTilemap = () => _.get(getKibanaCommonConfig(), 'tilemap', []); +export const getRegionmapLayers = () => { + const config = getKibanaCommonConfig(); + if (config.regionmap && config.regionmap.layers) { + return config.regionmap.layers; + } else { + return []; + } +}; +export const getTilemap = () => { + const config = getKibanaCommonConfig(); + if (config.tilemap) { + return config.tilemap; + } else { + return {}; + } +}; export const getShareService = () => pluginsStart.share; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 0d908356b714d..85b58da0ab09a 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import '../../index.scss'; export * from '../../embeddable/map_embeddable'; export * from '../../kibana_services'; export { renderApp } from '../../render_app'; diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.ts b/x-pack/plugins/maps/public/maps_vis_type_alias.ts index a3a8b55745d84..194b4595c0c93 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.ts +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.ts @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { +import type { VisualizationsSetup, VisualizationStage, } from '../../../../src/plugins/visualizations/public'; -import { SavedObject } from '../../../../src/core/types/saved_objects'; -import { MapSavedObject } from '../common/map_saved_object_type'; +import type { SavedObject } from '../../../../src/core/types/saved_objects'; +import type { MapSavedObject } from '../common/map_saved_object_type'; import { APP_ID, APP_ICON, diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index be2e097c71dc5..7ddab6bf509ff 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -5,19 +5,19 @@ * 2.0. */ -import { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; -import { UiActionsStart } from 'src/plugins/ui_actions/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; -import { DashboardStart } from 'src/plugins/dashboard/public'; -import { +import type { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; +import type { UiActionsStart } from 'src/plugins/ui_actions/public'; +import type { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; +import type { DashboardStart } from 'src/plugins/dashboard/public'; +import type { AppMountParameters, CoreSetup, CoreStart, Plugin, PluginInitializerContext, - DEFAULT_APP_CATEGORIES, } from '../../../../src/core/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; // @ts-ignore import { MapView } from './inspector/views/map_view'; import { @@ -29,8 +29,8 @@ import { } from './kibana_services'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { getMapsVisTypeAlias } from './maps_vis_type_alias'; -import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; -import { +import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import type { VisualizationsSetup, VisualizationsStart, } from '../../../../src/plugins/visualizations/public'; @@ -43,28 +43,27 @@ import { } from './url_generator'; import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action'; import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; -import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; +import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { MapsXPackConfig, MapsConfigType } from '../config'; import { getAppTitle } from '../common/i18n_getters'; import { lazyLoadMapModules } from './lazy_load_bundle'; import { MapsStartApi } from './api'; import { createLayerDescriptors, registerLayerWizard, registerSource } from './api'; -import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; -import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import type { MapsEmsPluginSetup } from '../../../../src/plugins/maps_ems/public'; -import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; -import { FileUploadPluginStart } from '../../file_upload/public'; -import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; -import { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; +import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import type { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; +import type { FileUploadPluginStart } from '../../file_upload/public'; +import type { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; +import type { PresentationUtilPluginStart } from '../../../../src/plugins/presentation_util/public'; import { getIsEnterprisePlus, registerLicensedFeatures, setLicensingPluginStart, } from './licensed_features'; import { EMSSettings } from '../common/ems_settings'; -import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; -import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; +import type { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; +import type { ChartsPluginStart } from '../../../../src/plugins/charts/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index 90dafa3afb67a..676ac6ce12efe 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -11,8 +11,6 @@ import { getMapsCapabilities } from '../kibana_services'; import { UPDATE_FLYOUT, - CLOSE_SET_VIEW, - OPEN_SET_VIEW, SET_IS_LAYER_TOC_OPEN, SET_FULL_SCREEN, SET_READ_ONLY, @@ -33,7 +31,6 @@ export type MapUiState = { isFullScreen: boolean; isReadOnly: boolean; isLayerTOCOpen: boolean; - isSetViewOpen: boolean; openTOCDetails: string[]; }; @@ -44,7 +41,6 @@ export const DEFAULT_MAP_UI_STATE = { isFullScreen: false, isReadOnly: !getMapsCapabilities().save, isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN, - isSetViewOpen: false, // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], @@ -55,10 +51,6 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) { switch (action.type) { case UPDATE_FLYOUT: return { ...state, flyoutDisplay: action.display }; - case CLOSE_SET_VIEW: - return { ...state, isSetViewOpen: false }; - case OPEN_SET_VIEW: - return { ...state, isSetViewOpen: true }; case SET_IS_LAYER_TOC_OPEN: return { ...state, isLayerTOCOpen: action.isLayerTOCOpen }; case SET_FULL_SCREEN: diff --git a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 597cd8e9c4287..7e0aa59756876 100644 --- a/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx @@ -201,7 +201,11 @@ export function getTopNavConfig({ options={tagSelector} /> ) : ( - + ); showSaveModal(saveModal, getCoreI18n().Context, PresentationUtilContext); diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts index dc34035c21b29..e5c83bd0f8f4a 100644 --- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -10,7 +10,6 @@ import { MapStoreState } from '../reducers/store'; import { FLYOUT_STATE } from '../reducers/ui'; export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; -export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; diff --git a/x-pack/plugins/maps/public/url_generator.ts b/x-pack/plugins/maps/public/url_generator.ts index c82af369fe113..9f28b388c4756 100644 --- a/x-pack/plugins/maps/public/url_generator.ts +++ b/x-pack/plugins/maps/public/url_generator.ts @@ -6,17 +6,17 @@ */ import rison from 'rison-node'; -import { +import type { TimeRange, Filter, Query, - esFilters, QueryState, RefreshInterval, } from '../../../../src/plugins/data/public'; +import { esFilters } from '../../../../src/plugins/data/public'; import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; -import { LayerDescriptor } from '../common/descriptor_types'; +import type { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; +import type { LayerDescriptor } from '../common/descriptor_types'; import { INITIAL_LAYERS_KEY } from '../common/constants'; import { lazyLoadMapModules } from './lazy_load_bundle'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx index cca9855ec1b69..443e2cfacbb5e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -48,7 +48,7 @@ const jobDetails: JobDetails = { }, [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: { helpText: i18n.translate('xpack.ml.dataframe.analytics.create.classificationHelpText', { - defaultMessage: 'Classification predicts labels of data points in the data set.', + defaultMessage: 'Classification predicts classes of data points in the data set.', }), icon: 'classificationJob', title: i18n.translate('xpack.ml.dataframe.analytics.create.classificationTitle', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 6bc838d87d630..e848f209516f4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -309,7 +309,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se @@ -320,7 +320,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se diff --git a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts index 567990aca0537..d07af8c878c51 100644 --- a/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/common/search_strategy/osquery/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { estypes } from '@elastic/elasticsearch'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { @@ -36,6 +37,7 @@ export type FactoryQueryTypes = OsqueryQueries; export interface RequestBasicOptions extends IEsSearchRequest { filterQuery: ESQuery | string | undefined; + aggregations?: Record; docValueFields?: DocValueFields[]; factoryQueryType?: FactoryQueryTypes; } diff --git a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx index 1880cec0ae8e2..660b837da6d93 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_table.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_table.tsx @@ -22,6 +22,7 @@ import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; import { Direction, ResultEdges } from '../../common/search_strategy'; import { useRouterNavigate } from '../common/lib/kibana'; +import { useOsqueryPolicies } from '../agents/use_osquery_policies'; const DataContext = createContext([]); @@ -91,12 +92,8 @@ const ActionResultsTableComponent: React.FC = ({ action setVisibleColumns, ]); - const { data: agentsData } = useAllAgents({ - activePage: 0, - limit: 1000, - direction: Direction.desc, - sortField: 'updated_at', - }); + const osqueryPolicyData = useOsqueryPolicies(); + const { agents } = useAllAgents(osqueryPolicyData); const renderCellValue: EuiDataGridProps['renderCellValue'] = useMemo( () => ({ rowIndex, columnId }) => { @@ -134,8 +131,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent_status') { const agentIdValue = value.fields?.agent_id[0]; - // @ts-expect-error update types - const agent = find(['_id', agentIdValue], agentsData?.agents); + const agent = find(['_id', agentIdValue], agents); const online = agent?.active; const color = online ? 'success' : 'danger'; const label = online ? 'Online' : 'Offline'; @@ -144,8 +140,7 @@ const ActionResultsTableComponent: React.FC = ({ action if (columnId === 'agent') { const agentIdValue = value.fields?.agent_id[0]; - // @ts-expect-error update types - const agent = find(['_id', agentIdValue], agentsData?.agents); + const agent = find(['_id', agentIdValue], agents); const agentName = agent?.local_metadata.host.name; // eslint-disable-next-line react-hooks/rules-of-hooks @@ -162,8 +157,7 @@ const ActionResultsTableComponent: React.FC = ({ action return '-'; }, - // @ts-expect-error update types - [actionId, agentsData?.agents, pagination.pageIndex, pagination.pageSize] + [actionId, agents, pagination.pageIndex, pagination.pageSize] ); const tableSorting: EuiDataGridSorting = useMemo( diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index e41b74c672e9b..5f1b6a0d2f0b1 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -5,158 +5,222 @@ * 2.0. */ -import { find } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; -import { - EuiBasicTable, - EuiBasicTableColumn, - EuiBasicTableProps, - EuiTableSelectionType, - EuiHealth, -} from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiHealth, EuiHighlight } from '@elastic/eui'; import { useAllAgents } from './use_all_agents'; -import { Direction } from '../../common/search_strategy'; +import { useAgentGroups } from './use_agent_groups'; +import { useOsqueryPolicies } from './use_osquery_policies'; import { Agent } from '../../common/shared_imports'; +import { + getNumAgentsInGrouping, + generateAgentCheck, + getNumOverlapped, + generateColorPicker, +} from './helpers'; -interface AgentsTableProps { - selectedAgents: string[]; - onChange: (payload: string[]) => void; +import { + ALL_AGENTS_LABEL, + AGENT_PLATFORMS_LABEL, + AGENT_POLICY_LABEL, + SELECT_AGENT_LABEL, + AGENT_SELECTION_LABEL, + generateSelectedAgentsMessage, +} from './translations'; + +import { AGENT_GROUP_KEY, SelectedGroups, AgentOptionValue, GroupOptionValue } from './types'; + +export interface AgentsSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; } -const AgentsTableComponent: React.FC = ({ selectedAgents, onChange }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('upgraded_at'); - const [sortDirection, setSortDirection] = useState(Direction.asc); - const [selectedItems, setSelectedItems] = useState([]); - const tableRef = useRef>(null); - - const onTableChange: EuiBasicTableProps['onChange'] = useCallback( - ({ page = {}, sort = {} }) => { - const { index: newPageIndex, size: newPageSize } = page; - - const { field: newSortField, direction: newSortDirection } = sort; +interface AgentsTableProps { + agentSelection: AgentsSelection; + onChange: (payload: AgentsSelection) => void; +} - setPageIndex(newPageIndex); - setPageSize(newPageSize); - setSortField(newSortField); - setSortDirection(newSortDirection); - }, - [] - ); +type GroupOption = EuiComboBoxOptionOption; - const onSelectionChange: EuiTableSelectionType<{}>['onSelectionChange'] = useCallback( - (newSelectedItems) => { - setSelectedItems(newSelectedItems); +const getColor = generateColorPicker(); - if (onChange) { - // @ts-expect-error update types - onChange(newSelectedItems.map((item) => item._id)); - } - }, - [onChange] +const AgentsTableComponent: React.FC = ({ onChange }) => { + const osqueryPolicyData = useOsqueryPolicies(); + const { loading: groupsLoading, totalCount: totalNumAgents, groups } = useAgentGroups( + osqueryPolicyData ); + const { agents } = useAllAgents(osqueryPolicyData); + const [loading, setLoading] = useState(true); + const [options, setOptions] = useState([]); + const [selectedOptions, setSelectedOptions] = useState([]); + const [numAgentsSelected, setNumAgentsSelected] = useState(0); - const renderStatus = (online: string) => { - const color = online ? 'success' : 'danger'; - const label = online ? 'Online' : 'Offline'; - return {label}; - }; - - const { data = {} } = useAllAgents({ - activePage: pageIndex, - limit: pageSize, - direction: sortDirection, - sortField, - }); - - const columns: Array> = useMemo( - () => [ - { - field: 'local_metadata.elastic.agent.id', - name: 'id', - sortable: true, - truncateText: true, - }, - { - field: 'local_metadata.host.name', - name: 'hostname', - truncateText: true, - }, - + useEffect(() => { + const allAgentsLabel = ALL_AGENTS_LABEL; + const opts: GroupOption[] = [ { - field: 'active', - name: 'Online', - dataType: 'boolean', - render: (active: string) => renderStatus(active), - }, - ], - [] - ); - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - // @ts-expect-error update types - totalItemCount: data.totalCount ?? 0, - pageSizeOptions: [3, 5, 8], - }), - // @ts-expect-error update types - [pageIndex, pageSize, data.totalCount] - ); - - const sorting = useMemo( - () => ({ - sort: { - field: sortField, - direction: sortDirection, + label: allAgentsLabel, + options: [ + { + label: allAgentsLabel, + value: { groupType: AGENT_GROUP_KEY.All, size: totalNumAgents }, + color: getColor(AGENT_GROUP_KEY.All), + }, + ], }, - }), - [sortDirection, sortField] - ); + ]; + + if (groups.platforms.length > 0) { + const groupType = AGENT_GROUP_KEY.Platform; + opts.push({ + label: AGENT_PLATFORMS_LABEL, + options: groups.platforms.map(({ name, size }) => ({ + label: name, + color: getColor(groupType), + value: { groupType, size }, + })), + }); + } - const selection: EuiBasicTableProps['selection'] = useMemo( - () => ({ - selectable: (agent: Agent) => agent.active, - selectableMessage: (selectable: boolean) => (!selectable ? 'User is currently offline' : ''), - onSelectionChange, - initialSelected: selectedItems, - }), - [onSelectionChange, selectedItems] - ); + if (groups.policies.length > 0) { + const groupType = AGENT_GROUP_KEY.Policy; + opts.push({ + label: AGENT_POLICY_LABEL, + options: groups.policies.map(({ name, size }) => ({ + label: name, + color: getColor(groupType), + value: { groupType, size }, + })), + }); + } - useEffect(() => { - if ( - selectedAgents?.length && - // @ts-expect-error update types - data.agents?.length && - selectedItems.length !== selectedAgents.length - ) { - tableRef?.current?.setSelection( - // @ts-expect-error update types - selectedAgents.map((agentId) => find({ _id: agentId }, data.agents)) - ); + if (agents && agents.length > 0) { + const groupType = AGENT_GROUP_KEY.Agent; + opts.push({ + label: AGENT_SELECTION_LABEL, + options: (agents as Agent[]).map((agent: Agent) => ({ + label: agent.local_metadata.host.hostname, + color: getColor(groupType), + value: { + groupType, + groups: { policy: agent.policy_id ?? '', platform: agent.local_metadata.os.platform }, + id: agent.local_metadata.elastic.agent.id, + online: agent.active, + }, + })), + }); } - // @ts-expect-error update types - }, [selectedAgents, data.agents, selectedItems.length]); + setLoading(false); + setOptions(opts); + }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents]); + + const onSelection = useCallback( + (selection: GroupOption[]) => { + // TODO?: optimize this by making it incremental + const newAgentSelection: AgentsSelection = { + agents: [], + allAgentsSelected: false, + platformsSelected: [], + policiesSelected: [], + }; + // parse through the selections to be able to determine how many are actually selected + const selectedAgents = []; + const selectedGroups: SelectedGroups = { + policy: {}, + platform: {}, + }; + + // TODO: clean this up, make it less awkward + for (const opt of selection) { + const groupType = opt.value?.groupType; + let value; + switch (groupType) { + case AGENT_GROUP_KEY.All: + newAgentSelection.allAgentsSelected = true; + break; + case AGENT_GROUP_KEY.Platform: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.platform[opt.label] = value.size; + } + newAgentSelection.platformsSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Policy: + value = opt.value as GroupOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to calculate diffs when all agents are selected + selectedGroups.policy[opt.label] = value.size ?? 0; + } + newAgentSelection.policiesSelected.push(opt.label); + break; + case AGENT_GROUP_KEY.Agent: + value = opt.value as AgentOptionValue; + if (!newAgentSelection.allAgentsSelected) { + // we don't need to count how many agents are selected if they are all selected + selectedAgents.push(opt.value); + } + // TODO: fix this casting by updating the opt type to be a union + newAgentSelection.agents.push(value.id as string); + break; + default: + // this should never happen! + // eslint-disable-next-line no-console + console.error(`unknown group type ${groupType}`); + } + } + if (newAgentSelection.allAgentsSelected) { + setNumAgentsSelected(totalNumAgents); + } else { + const checkAgent = generateAgentCheck(selectedGroups); + setNumAgentsSelected( + // filter out all the agents counted by selected policies and platforms + selectedAgents.filter((a) => checkAgent(a as AgentOptionValue)).length + + // add the number of agents added via policy and platform groups + getNumAgentsInGrouping(selectedGroups) - + // subtract the number of agents double counted by policy/platform selections + getNumOverlapped(selectedGroups, groups.overlap) + ); + } + onChange(newAgentSelection); + setSelectedOptions(selection); + }, + [groups, onChange, totalNumAgents] + ); + const renderOption = useCallback((option, searchValue, contentClassName) => { + const { label, value } = option; + return value?.groupType === AGENT_GROUP_KEY.Agent ? ( + + + {label} + + + ) : ( + + {label} +   + ({value?.size}) + + ); + }, []); return ( - - ref={tableRef} - // @ts-expect-error update types - // eslint-disable-next-line react-perf/jsx-no-new-array-as-prop - items={data.agents ?? []} - itemId="_id" - columns={columns} - pagination={pagination} - sorting={sorting} - isSelectable={true} - selection={selection} - onChange={onTableChange} - rowHeader="firstName" - /> +
+

{SELECT_AGENT_LABEL}

+ {numAgentsSelected > 0 ? {generateSelectedAgentsMessage(numAgentsSelected)} : ''} +   + +
); }; diff --git a/x-pack/plugins/osquery/public/agents/helpers.test.ts b/x-pack/plugins/osquery/public/agents/helpers.test.ts new file mode 100644 index 0000000000000..3efd1b877d1a0 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/helpers.test.ts @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getNumOverlapped, getNumAgentsInGrouping, processAggregations } from './helpers'; +import { Overlap, SelectedGroups } from './types'; + +describe('processAggregations', () => { + it('should handle empty inputs properly', () => { + const input = {}; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([]); + expect(policies).toEqual([]); + expect(overlap).toEqual({}); + }); + it('should handle platforms with no policies', () => { + const input = { + platforms: { + buckets: [ + { + key: 'darwin', + doc_count: 200, + policies: { + buckets: [], + }, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([ + { + name: 'darwin', + size: 200, + }, + ]); + expect(policies).toEqual([]); + expect(overlap).toEqual({}); + }); + it('should handle policies with no platforms', () => { + const input = { + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([]); + expect(policies).toEqual([ + { + name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + { + name: '8cd06880-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + ]); + expect(overlap).toEqual({}); + }); + it('should parse aggregation responses down into metadata objects', () => { + const input = { + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + platforms: { + buckets: [ + { + key: 'darwin', + doc_count: 200, + policies: { + buckets: [ + { + key: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + { + key: '8cd06880-8a74-11eb-86cb-c58693443a4f', + doc_count: 100, + }, + ], + }, + }, + ], + }, + }; + const { platforms, policies, overlap } = processAggregations(input); + expect(platforms).toEqual([ + { + name: 'darwin', + size: 200, + }, + ]); + expect(policies).toEqual([ + { + name: '8cd01a60-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + { + name: '8cd06880-8a74-11eb-86cb-c58693443a4f', + size: 100, + }, + ]); + expect(overlap).toEqual({ + darwin: { + '8cd06880-8a74-11eb-86cb-c58693443a4f': 100, + '8cd01a60-8a74-11eb-86cb-c58693443a4f': 100, + }, + }); + }); +}); + +describe('getNumAgentsInGrouping', () => { + it('should handle empty objects', () => { + const selectedGroups: SelectedGroups = {}; + expect(getNumAgentsInGrouping(selectedGroups)).toEqual(0); + }); + + it('should add up the quantities for the selected groups', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + }, + policy: { + policy_id1: 40, + }, + }; + expect(getNumAgentsInGrouping(selectedGroups)).toEqual(75); + }); +}); + +describe('getNumOverlapped', () => { + const overlap: Overlap = { + darwin: { + policy_id1: 15, + policy_id2: 35, + }, + linux: { + policy_id1: 25, + policy_id2: 10, + }, + }; + + it('should add up the quantities associated with a platform/policy selection', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + }, + policy: { + policy_id1: 40, + }, + }; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(25); + }); + + it('should gracefully handle empty objects', () => { + const selectedGroups: SelectedGroups = {}; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing platforms', () => { + const selectedGroups: SelectedGroups = { + policy: { + policy_id1: 40, + policy_id3: 40, + }, + }; + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing policies', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + windows: 40, + }, + }; + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(0); + }); + + it('should gracefully handle missing group selections', () => { + const selectedGroups: SelectedGroups = { + platform: { + linux: 35, + windows: 40, + }, + policy: { + policy_id1: 40, + policy_id3: 40, + }, + }; + + const computedOverlap = getNumOverlapped(selectedGroups, overlap); + expect(computedOverlap).toBe(25); + }); +}); diff --git a/x-pack/plugins/osquery/public/agents/helpers.ts b/x-pack/plugins/osquery/public/agents/helpers.ts index fef17aadb62be..830fca5f57caa 100644 --- a/x-pack/plugins/osquery/public/agents/helpers.ts +++ b/x-pack/plugins/osquery/public/agents/helpers.ts @@ -5,15 +5,97 @@ * 2.0. */ +import { Aggregate, TermsAggregate } from '@elastic/elasticsearch/api/types'; +import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { PaginationInputPaginated, FactoryQueryTypes, StrategyResponseType, Inspect, } from '../../common/search_strategy'; +import { + AGENT_GROUP_KEY, + SelectedGroups, + Overlap, + Group, + AgentOptionValue, + AggregationDataPoint, +} from './types'; export type InspectResponse = Inspect & { response: string[] }; +export const getNumOverlapped = ( + { policy = {}, platform = {} }: SelectedGroups, + overlap: Overlap +) => { + let sum = 0; + Object.keys(platform).forEach((plat) => { + const policies = overlap[plat] ?? {}; + Object.keys(policy).forEach((pol) => { + sum += policies[pol] ?? 0; + }); + }); + return sum; +}; +export const processAggregations = (aggs: Record) => { + const platforms: Group[] = []; + const overlap: Overlap = {}; + const platformTerms = aggs.platforms as TermsAggregate; + const policyTerms = aggs.policies as TermsAggregate; + + const policies = policyTerms?.buckets.map((o) => ({ name: o.key, size: o.doc_count })) ?? []; + + if (platformTerms?.buckets) { + for (const { key, doc_count: size, policies: platformPolicies } of platformTerms.buckets) { + platforms.push({ name: key, size }); + if (platformPolicies?.buckets && policies.length > 0) { + overlap[key] = platformPolicies.buckets.reduce((acc: { [key: string]: number }, pol) => { + acc[pol.key] = pol.doc_count; + return acc; + }, {} as { [key: string]: number }); + } + } + } + + return { + platforms, + overlap, + policies, + }; +}; +export const generateColorPicker = () => { + const visColorsBehindText = euiPaletteColorBlindBehindText(); + const typeColors = new Map(); + return (type: AGENT_GROUP_KEY) => { + if (!typeColors.has(type)) { + typeColors.set(type, visColorsBehindText[typeColors.size]); + } + return typeColors.get(type); + }; +}; + +export const getNumAgentsInGrouping = (selectedGroups: SelectedGroups) => { + let sum = 0; + Object.keys(selectedGroups).forEach((g) => { + const group = selectedGroups[g]; + sum += Object.keys(group).reduce((acc, k) => acc + group[k], 0); + }); + return sum; +}; + +export const generateAgentCheck = (selectedGroups: SelectedGroups) => { + return ({ groups }: AgentOptionValue) => { + return Object.keys(groups) + .map((group) => { + const selectedGroup = selectedGroups[group]; + const agentGroup = groups[group]; + // check if the agent platform/policy is selected + return selectedGroup[agentGroup]; + }) + .every((a) => !a); + }; +}; + export const generateTablePaginationOptions = ( activePage: number, limit: number, diff --git a/x-pack/plugins/osquery/public/agents/translations.ts b/x-pack/plugins/osquery/public/agents/translations.ts index 0d9d9a8a12b8f..af99a73d63de2 100644 --- a/x-pack/plugins/osquery/public/agents/translations.ts +++ b/x-pack/plugins/osquery/public/agents/translations.ts @@ -7,6 +7,42 @@ import { i18n } from '@kbn/i18n'; +export const generateSelectedAgentsMessage = (numAgents: number): string => { + if (numAgents === 0) { + return ''; + } else if (numAgents === 1) { + return i18n.translate('xpack.osquery.agents.oneSelectedAgentText', { + defaultMessage: '{numAgents} agent selected.', + values: { numAgents }, + }); + } else { + return i18n.translate('xpack.osquery.agents.mulitpleSelectedAgentsText', { + defaultMessage: '{numAgents} agents selected.', + values: { numAgents }, + }); + } +}; + +export const ALL_AGENTS_LABEL = i18n.translate('xpack.osquery.agents.allAgentsLabel', { + defaultMessage: `All agents`, +}); + +export const AGENT_PLATFORMS_LABEL = i18n.translate('xpack.osquery.agents.platformLabel', { + defaultMessage: `Platform`, +}); + +export const AGENT_POLICY_LABEL = i18n.translate('xpack.osquery.agents.policyLabel', { + defaultMessage: `Policy`, +}); + +export const AGENT_SELECTION_LABEL = i18n.translate('xpack.osquery.agents.selectionLabel', { + defaultMessage: `Agents`, +}); + +export const SELECT_AGENT_LABEL = i18n.translate('xpack.osquery.agents.selectAgentLabel', { + defaultMessage: `Select Agents`, +}); + export const ERROR_ALL_AGENTS = i18n.translate('xpack.osquery.agents.errorSearchDescription', { defaultMessage: `An error has occurred on all agents search`, }); diff --git a/x-pack/plugins/osquery/public/agents/types.ts b/x-pack/plugins/osquery/public/agents/types.ts new file mode 100644 index 0000000000000..2fa8ddaf345cd --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/types.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TermsAggregate } from '@elastic/elasticsearch/api/types'; + +interface BaseDataPoint { + key: string; + doc_count: number; +} + +export type AggregationDataPoint = BaseDataPoint & { + [key: string]: TermsAggregate; +}; + +export interface Group { + name: string; + size: number; +} +export interface Overlap { + [platform: string]: { [policy: string]: number }; +} + +export interface SelectedGroups { + [groupType: string]: { [groupName: string]: number }; +} + +interface BaseGroupOption { + groupType: AGENT_GROUP_KEY; +} + +export type AgentOptionValue = BaseGroupOption & { + groups: { [groupType: string]: string }; + online: boolean; + id: string; +}; + +export type GroupOptionValue = BaseGroupOption & { + size: number; +}; + +export enum AGENT_GROUP_KEY { + All, + Platform, + Policy, + Agent, +} diff --git a/x-pack/plugins/osquery/public/agents/use_agent_groups.ts b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts new file mode 100644 index 0000000000000..0eaca65d02d4b --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_agent_groups.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useState } from 'react'; +import { useQuery } from 'react-query'; +import { useKibana } from '../common/lib/kibana'; + +import { + OsqueryQueries, + AgentsRequestOptions, + AgentsStrategyResponse, +} from '../../common/search_strategy'; + +import { generateTablePaginationOptions, processAggregations } from './helpers'; +import { Overlap, Group } from './types'; + +interface UseAgentGroups { + osqueryPolicies: string[]; + osqueryPoliciesLoading: boolean; +} + +export const useAgentGroups = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAgentGroups) => { + const { data } = useKibana().services; + + const [platforms, setPlatforms] = useState([]); + const [policies, setPolicies] = useState([]); + const [loading, setLoading] = useState(true); + const [overlap, setOverlap] = useState(() => ({})); + const [totalCount, setTotalCount] = useState(0); + useQuery( + ['agentGroups'], + async () => { + const responseData = await data.search + .search( + { + filterQuery: { terms: { policy_id: osqueryPolicies } }, + factoryQueryType: OsqueryQueries.agents, + aggregations: { + platforms: { + terms: { + field: 'local_metadata.os.platform', + }, + aggs: { + policies: { + terms: { + field: 'policy_id', + }, + }, + }, + }, + policies: { + terms: { + field: 'policy_id', + }, + }, + }, + pagination: generateTablePaginationOptions(0, 9000), + sort: { + direction: 'asc', + field: 'local_metadata.os.platform', + }, + } as AgentsRequestOptions, + { + strategy: 'osquerySearchStrategy', + } + ) + .toPromise(); + + if (responseData.rawResponse.aggregations) { + const { + platforms: newPlatforms, + overlap: newOverlap, + policies: newPolicies, + } = processAggregations(responseData.rawResponse.aggregations); + + setPlatforms(newPlatforms); + setOverlap(newOverlap); + setPolicies(newPolicies); + } + + setLoading(false); + setTotalCount(responseData.totalCount); + }, + { + enabled: !osqueryPoliciesLoading, + } + ); + + return { + loading, + totalCount, + groups: { + platforms, + policies, + overlap, + }, + }; +}; diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index 663c6936fe55b..607f9ae007692 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -5,94 +5,32 @@ * 2.0. */ -import deepEqual from 'fast-deep-equal'; -import { useEffect, useState } from 'react'; import { useQuery } from 'react-query'; -import { createFilter } from '../common/helpers'; import { useKibana } from '../common/lib/kibana'; -import { - PageInfoPaginated, - OsqueryQueries, - AgentsRequestOptions, - AgentsStrategyResponse, - Direction, -} from '../../common/search_strategy'; -import { ESTermQuery } from '../../common/typed_json'; -import { Agent } from '../../common/shared_imports'; - -import { generateTablePaginationOptions, getInspectResponse, InspectResponse } from './helpers'; - -export interface AgentsArgs { - agents: Agent[]; - id: string; - inspect: InspectResponse; - isInspected: boolean; - pageInfo: PageInfoPaginated; - totalCount: number; -} interface UseAllAgents { - activePage: number; - direction: Direction; - limit: number; - sortField: string; - filterQuery?: ESTermQuery | string; - skip?: boolean; + osqueryPolicies: string[]; + osqueryPoliciesLoading: boolean; } -export const useAllAgents = ({ - activePage, - direction, - limit, - sortField, - filterQuery, - skip = false, -}: UseAllAgents) => { - const { data } = useKibana().services; - - const [agentsRequest, setHostRequest] = useState(null); - - const response = useQuery( - ['agents', { activePage, direction, limit, sortField }], +export const useAllAgents = ({ osqueryPolicies, osqueryPoliciesLoading }: UseAllAgents) => { + // TODO: properly fetch these in an async manner + const { http } = useKibana().services; + const { isLoading: agentsLoading, data: agentData } = useQuery( + ['agents', osqueryPolicies], async () => { - if (!agentsRequest) return Promise.resolve(); - - const responseData = await data.search - .search(agentsRequest, { - strategy: 'osquerySearchStrategy', - }) - .toPromise(); - - return { - ...responseData, - agents: responseData.edges, - inspect: getInspectResponse(responseData), - }; + return await http.get('/api/fleet/agents', { + query: { + kuery: osqueryPolicies.map((p) => `policy_id:${p}`).join(' or '), + perPage: 9000, + }, + }); }, { - enabled: !skip && !!agentsRequest, + enabled: !osqueryPoliciesLoading, } ); - useEffect(() => { - setHostRequest((prevRequest) => { - const myRequest = { - ...(prevRequest ?? {}), - factoryQueryType: OsqueryQueries.agents, - filterQuery: createFilter(filterQuery), - pagination: generateTablePaginationOptions(activePage, limit), - sort: { - direction, - field: sortField, - }, - }; - if (!deepEqual(prevRequest, myRequest)) { - return myRequest; - } - return prevRequest; - }); - }, [activePage, direction, filterQuery, limit, sortField]); - - return response; + return { agentsLoading, agents: agentData?.list }; }; diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts new file mode 100644 index 0000000000000..f786e9167d2f8 --- /dev/null +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from 'react-query'; +import { useKibana } from '../common/lib/kibana'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; + +export const useOsqueryPolicies = () => { + const { http } = useKibana().services; + + const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies } = useQuery( + ['osqueryPolicies'], + async () => { + return await http.get('/api/fleet/package_policies', { + query: { + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:osquery_manager`, + }, + }); + }, + { select: (data) => data.items.map((p: { policy_id: string }) => p.policy_id) } + ); + + return { osqueryPoliciesLoading, osqueryPolicies }; +}; diff --git a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx index 7a93b5d2491db..4bc9262af7613 100644 --- a/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx +++ b/x-pack/plugins/osquery/public/live_query/form/agents_table_field.tsx @@ -7,10 +7,10 @@ import React, { useCallback } from 'react'; import { FieldHook } from '../../shared_imports'; -import { AgentsTable } from '../../agents/agents_table'; +import { AgentsTable, AgentsSelection } from '../../agents/agents_table'; interface AgentsTableFieldProps { - field: FieldHook; + field: FieldHook; } const AgentsTableFieldComponent: React.FC = ({ field }) => { @@ -24,7 +24,7 @@ const AgentsTableFieldComponent: React.FC = ({ field }) = [value, setValue] ); - return ; + return ; }; export const AgentsTableField = React.memo(AgentsTableFieldComponent); diff --git a/x-pack/plugins/osquery/public/live_query/form/index.tsx b/x-pack/plugins/osquery/public/live_query/form/index.tsx index 4a69e2fc0e76d..7e19bee530ec5 100644 --- a/x-pack/plugins/osquery/public/live_query/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_query/form/index.tsx @@ -40,7 +40,7 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, on return (
- + diff --git a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts new file mode 100644 index 0000000000000..975770e594367 --- /dev/null +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; +import { OsqueryAppContext } from './osquery_app_context_services'; + +export interface AgentSelection { + agents: string[]; + allAgentsSelected: boolean; + platformsSelected: string[]; + policiesSelected: string[]; +} + +export const parseAgentSelection = async ( + esClient: ElasticsearchClient, + context: OsqueryAppContext, + agentSelection: AgentSelection +) => { + let selectedAgents: string[] = []; + const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection; + const agentService = context.service.getAgentService(); + if (agentService) { + if (allAgentsSelected) { + // TODO: actually fetch all the agents + const { agents: fetchedAgents } = await agentService.listAgents(esClient, { + perPage: 9000, + showInactive: true, + }); + selectedAgents.push(...fetchedAgents.map((a) => a.id)); + } else { + if (platformsSelected.length > 0 || policiesSelected.length > 0) { + const kueryFragments = []; + if (platformsSelected.length) { + kueryFragments.push( + ...platformsSelected.map((platform) => `local_metadata.os.platform:${platform}`) + ); + } + if (policiesSelected.length) { + kueryFragments.push(...policiesSelected.map((policy) => `policy_id:${policy}`)); + } + const kuery = kueryFragments.join(' or '); + // TODO: actually fetch all the agents + const { agents: fetchedAgents } = await agentService.listAgents(esClient, { + kuery, + perPage: 9000, + showInactive: true, + }); + selectedAgents.push(...fetchedAgents.map((a) => a.id)); + } + selectedAgents.push(...agents); + selectedAgents = Array.from(new Set(selectedAgents)); + } + } + return selectedAgents; +}; diff --git a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts index 25212bc3bf5cc..7068243cc0fb7 100644 --- a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts +++ b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts @@ -12,8 +12,11 @@ import moment from 'moment'; import { IRouter } from '../../../../../../src/core/server'; import { packSavedObjectType, savedQuerySavedObjectType } from '../../../common/types'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const createActionRoute = (router: IRouter) => { +import { parseAgentSelection, AgentSelection } from '../../lib/parse_agent_groups'; + +export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( { path: '/internal/osquery/action', @@ -24,7 +27,8 @@ export const createActionRoute = (router: IRouter) => { }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asInternalUser; - + const { agentSelection } = request.body as { agentSelection: AgentSelection }; + const selectedAgents = await parseAgentSelection(esClient, osqueryContext, agentSelection); // @ts-expect-error update validation if (request.body.pack_id) { const savedObjectsClient = context.core.savedObjects.client; @@ -72,8 +76,7 @@ export const createActionRoute = (router: IRouter) => { expiration: moment().add(2, 'days').toISOString(), type: 'INPUT_ACTION', input_type: 'osquery', - // @ts-expect-error update validation - agents: request.body.agents, + agents: selectedAgents, data: { id: query.id, // @ts-expect-error update validation @@ -103,8 +106,7 @@ export const createActionRoute = (router: IRouter) => { expiration: moment().add(2, 'days').toISOString(), type: 'INPUT_ACTION', input_type: 'osquery', - // @ts-expect-error update validation - agents: request.body.agents, + agents: selectedAgents, data: { // @ts-expect-error update validation id: request.body.query.id ?? uuid.v4(), diff --git a/x-pack/plugins/osquery/server/routes/action/index.ts b/x-pack/plugins/osquery/server/routes/action/index.ts index 37e04fac5b986..fcf89d79dd0ee 100644 --- a/x-pack/plugins/osquery/server/routes/action/index.ts +++ b/x-pack/plugins/osquery/server/routes/action/index.ts @@ -7,7 +7,8 @@ import { IRouter } from '../../../../../../src/core/server'; import { createActionRoute } from './create_action_route'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const initActionRoutes = (router: IRouter) => { - createActionRoute(router); +export const initActionRoutes = (router: IRouter, context: OsqueryAppContext) => { + createActionRoute(router, context); }; diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts index 29df227583992..59d4085a77be1 100644 --- a/x-pack/plugins/osquery/server/routes/index.ts +++ b/x-pack/plugins/osquery/server/routes/index.ts @@ -13,7 +13,7 @@ import { OsqueryAppContext } from '../lib/osquery_app_context_services'; import { initPackRoutes } from './pack'; export const defineRoutes = (router: IRouter, context: OsqueryAppContext) => { - initActionRoutes(router); + initActionRoutes(router, context); initPackRoutes(router); initSavedQueryRoutes(router); initScheduledQueryRoutes(router, context); diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts index 4ad6022017966..52101462270c7 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/factory/agents/query.all_agents.dsl.ts @@ -7,15 +7,18 @@ import { ISearchRequestParams } from '../../../../../../../../src/plugins/data/common'; import { AgentsRequestOptions } from '../../../../../common/search_strategy'; -// import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; +import { createQueryFilterClauses } from '../../../../../common/utils/build_query'; export const buildAgentsQuery = ({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars filterQuery, pagination: { cursorStart, querySize }, sort, + aggregations, }: AgentsRequestOptions): ISearchRequestParams => { - // const filter = [...createQueryFilterClauses(filterQuery)]; + const filter = [ + { term: { active: { value: 'true' } } }, + ...createQueryFilterClauses(filterQuery), + ]; const dslQuery = { allowNoIndices: true, @@ -23,12 +26,11 @@ export const buildAgentsQuery = ({ ignoreUnavailable: true, body: { query: { - term: { - active: { - value: 'true', - }, + bool: { + filter, }, }, + aggs: aggregations, track_total_hits: true, sort: [ { diff --git a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx index 546d6cf5a6bba..56eb784467c05 100644 --- a/x-pack/plugins/security/public/nav_control/nav_control_component.tsx +++ b/x-pack/plugins/security/public/nav_control/nav_control_component.tsx @@ -15,7 +15,6 @@ import { EuiIcon, EuiLoadingSpinner, EuiPopover, - EuiText, } from '@elastic/eui'; import React, { Component } from 'react'; import type { Observable, Subscription } from 'rxjs'; @@ -128,7 +127,7 @@ export class SecurityNavControl extends Component { const userMenuLinkMenuItems = userMenuLinks .sort(({ order: orderA = Infinity }, { order: orderB = Infinity }) => orderA - orderB) .map(({ label, iconType, href }: UserMenuLink) => ({ - name: {label}, + name: label, icon: , href, 'data-test-subj': `userMenuLink__${label}`, diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index e1e78f8e310e1..129d592edd264 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -502,7 +502,7 @@ describe('indicator match', () => { { line: 3, text: - ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', + ' "indicator": "{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\",\\"event\\":{\\"reference\\":\\"https://urlhaus-api.abuse.ch/v1/download/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/\\",\\"ingested\\":\\"2021-03-10T14:51:09.809069Z\\",\\"created\\":\\"2021-03-10T14:51:07.663Z\\",\\"kind\\":\\"enrichment\\",\\"module\\":\\"threatintel\\",\\"category\\":\\"threat\\",\\"type\\":\\"indicator\\",\\"dataset\\":\\"threatintel.abusemalware\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"filebeat-7.12.0-2021.03.10-000001\\",\\"type\\":\\"file\\"}}"', }, { line: 2, text: ' }' }, ]; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index eceea1de4edc0..297746fd23632 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -12,15 +12,11 @@ import minimatch from 'minimatch'; import { IndexPatternMapping } from './types'; import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/public'; -import { +import type { + RenderTooltipContentParams, MapEmbeddable, MapEmbeddableInput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/maps/public/embeddable'; -import { - RenderTooltipContentParams, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/maps/public/classes/tooltips/tooltip_property'; +} from '../../../../../../plugins/maps/public'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts index 7d9c66261924b..6317cad7f8d98 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/types.ts @@ -5,8 +5,7 @@ * 2.0. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderTooltipContentParams } from '../../../../../maps/public/classes/tooltips/tooltip_property'; +import type { RenderTooltipContentParams } from '../../../../../maps/public'; export interface IndexPatternMapping { title: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index c6f432a28aee4..326d5777543be 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -21,7 +21,7 @@ import ecsMapping from './ecs_mapping.json'; incremented by 10 in order to add "room" for the aforementioned patch release */ -export const SIGNALS_TEMPLATE_VERSION = 25; +export const SIGNALS_TEMPLATE_VERSION = 26; export const MIN_EQL_RULE_INDEX_VERSION = 2; export const getSignalsTemplate = (index: string) => { @@ -45,6 +45,19 @@ export const getSignalsTemplate = (index: string) => { properties: { ...ecsMapping.mappings.properties, signal: signalsMapping.mappings.properties.signal, + threat: { + ...ecsMapping.mappings.properties.threat, + properties: { + ...ecsMapping.mappings.properties.threat.properties, + indicator: { + ...ecsMapping.mappings.properties.threat.properties.indicator, + properties: { + ...ecsMapping.mappings.properties.threat.properties.indicator.properties, + event: ecsMapping.mappings.properties.event, + }, + }, + }, + }, }, _meta: { version: SIGNALS_TEMPLATE_VERSION, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index 7b3ca099cc93c..7c80572f6b1ee 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -83,6 +83,7 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { + event: { dataset: 'abuse.ch', reference: 'https://test.com' }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), @@ -117,6 +118,16 @@ describe('buildMatchedIndicator', () => { expect(get(indicator, 'matched.atomic')).toEqual('domain_1'); }); + it('returns event values as a part of threat', () => { + const [indicator] = buildMatchedIndicator({ + queries, + threats, + indicatorPath, + }); + const expectedEvent = threats[0]._source!.event; + expect(get(indicator, 'event')).toEqual(expectedEvent); + }); + it('returns the _id of the matched indicator as matched.id', () => { const [indicator] = buildMatchedIndicator({ queries, @@ -162,12 +173,16 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { - threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, + event: { reference: 'https://test.com' }, + threat: { + indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' }, + }, }, }), getThreatListItemMock({ _id: '456', _source: { + event: { reference: 'https://test2.com' }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), @@ -205,6 +220,10 @@ describe('buildMatchedIndicator', () => { }, other: 'other_1', type: 'type_1', + event: { + reference: 'https://test.com', + dataset: 'abuse.ch', + }, }, ]); }); @@ -214,6 +233,9 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { + event: { + reference: 'https://test3.com', + }, 'threat.indicator.domain': 'domain_1', custom: { indicator: { @@ -244,6 +266,9 @@ describe('buildMatchedIndicator', () => { type: 'indicator_type', }, type: 'indicator_type', + event: { + reference: 'https://test3.com', + }, }, ]); }); @@ -307,6 +332,9 @@ describe('buildMatchedIndicator', () => { getThreatListItemMock({ _id: '123', _source: { + event: { + reference: 'https://test4.com', + }, threat: { indicator: [ { domain: 'foo', type: 'first' }, @@ -334,6 +362,9 @@ describe('buildMatchedIndicator', () => { type: 'first', }, type: 'first', + event: { + reference: 'https://test4.com', + }, }, ]); }); @@ -392,6 +423,9 @@ describe('enrichSignalThreatMatches', () => { getThreatListItemMock({ _id: '123', _source: { + event: { + category: 'malware', + }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), @@ -419,7 +453,11 @@ describe('enrichSignalThreatMatches', () => { it('preserves existing threat.indicator objects on signals', async () => { const signalHit = getSignalHitMock({ - _source: { '@timestamp': 'mocked', threat: { indicator: [{ existing: 'indicator' }] } }, + _source: { + '@timestamp': 'mocked', + event: { category: 'malware' }, + threat: { indicator: [{ existing: 'indicator' }] }, + }, matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); @@ -444,6 +482,9 @@ describe('enrichSignalThreatMatches', () => { }, other: 'other_1', type: 'type_1', + event: { + category: 'malware', + }, }, ]); }); @@ -477,7 +518,11 @@ describe('enrichSignalThreatMatches', () => { it('preserves an existing threat.indicator object on signals', async () => { const signalHit = getSignalHitMock({ - _source: { '@timestamp': 'mocked', threat: { indicator: { existing: 'indicator' } } }, + _source: { + '@timestamp': 'mocked', + event: { category: 'virus' }, + threat: { indicator: { existing: 'indicator' } }, + }, matched_queries: [matchedQuery], }); const signals = getSignalsResponseMock([signalHit]); @@ -502,6 +547,9 @@ describe('enrichSignalThreatMatches', () => { }, other: 'other_1', type: 'type_1', + event: { + category: 'malware', + }, }, ]); }); @@ -573,12 +621,14 @@ describe('enrichSignalThreatMatches', () => { getThreatListItemMock({ _id: '123', _source: { + event: { category: 'threat' }, threat: { indicator: { domain: 'domain_1', other: 'other_1', type: 'type_1' } }, }, }), getThreatListItemMock({ _id: '456', _source: { + event: { category: 'bad' }, threat: { indicator: { domain: 'domain_2', other: 'other_2', type: 'type_2' } }, }, }), @@ -622,6 +672,9 @@ describe('enrichSignalThreatMatches', () => { field: 'event.field', type: 'type_1', }, + event: { + category: 'threat', + }, other: 'other_1', type: 'type_1', }, @@ -634,6 +687,9 @@ describe('enrichSignalThreatMatches', () => { field: 'event.other', type: 'type_2', }, + event: { + category: 'bad', + }, other: 'other_2', type: 'type_2', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index 83a3ce8cb773f..c26f03d1dd480 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -57,9 +57,11 @@ export const buildMatchedIndicator = ({ } const atomic = get(matchedThreat?._source, query.value) as unknown; const type = get(indicator, 'type') as unknown; + const event = get(matchedThreat?._source, 'event') as unknown; return { ...indicator, + event, matched: { atomic, field: query.field, id: query.id, index: query.index, type }, }; }); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts index 335b18de92850..45b8b23cae477 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts @@ -108,6 +108,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setRestoreSnapshotResponse = (response?: HttpResponse) => { + server.respondWith('POST', `${API_BASE_PATH}restore/:repository/:snapshot`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + return { setLoadRepositoriesResponse, setLoadRepositoryTypesResponse, @@ -119,6 +127,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setAddPolicyResponse, setGetPolicyResponse, setCleanupRepositoryResponse, + setRestoreSnapshotResponse, }; }; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts index c0ffae81a4258..5bc970a1143a4 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -25,6 +25,7 @@ const initTestBed = registerTestBed( const setupActions = (testBed: TestBed) => { const { find, component, form } = testBed; + return { findDataStreamCallout() { return find('dataStreamWarningCallOut'); @@ -37,6 +38,28 @@ const setupActions = (testBed: TestBed) => { component.update(); }, + + toggleIncludeAliases() { + act(() => { + form.toggleEuiSwitch('includeAliasesSwitch'); + }); + + component.update(); + }, + + goToStep(step: number) { + while (--step > 0) { + find('nextButton').simulate('click'); + } + component.update(); + }, + + async clickRestore() { + await act(async () => { + find('restoreButton').simulate('click'); + }); + component.update(); + }, }; }; @@ -58,5 +81,8 @@ export const setup = async (): Promise => { export type RestoreSnapshotFormTestSubject = | 'snapshotRestoreStepLogistics' | 'includeGlobalStateSwitch' + | 'includeAliasesSwitch' + | 'nextButton' + | 'restoreButton' | 'systemIndicesInfoCallOut' | 'dataStreamWarningCallOut'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts index 2fecce36f09df..9f12415b70a9f 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts @@ -33,7 +33,7 @@ describe('', () => { testBed.component.update(); }); - it('shows the data streams warning when the snapshot has data streams', () => { + test('shows the data streams warning when the snapshot has data streams', () => { const { exists } = testBed; expect(exists('dataStreamWarningCallOut')).toBe(true); }); @@ -49,7 +49,7 @@ describe('', () => { testBed.component.update(); }); - it('hides the data streams warning when the snapshot has data streams', () => { + test('hides the data streams warning when the snapshot has data streams', () => { const { exists } = testBed; expect(exists('dataStreamWarningCallOut')).toBe(false); }); @@ -65,7 +65,7 @@ describe('', () => { testBed.component.update(); }); - it('shows an info callout when include_global_state is enabled', () => { + test('shows an info callout when include_global_state is enabled', () => { const { exists, actions } = testBed; expect(exists('systemIndicesInfoCallOut')).toBe(false); @@ -75,4 +75,30 @@ describe('', () => { expect(exists('systemIndicesInfoCallOut')).toBe(true); }); }); + + // NOTE: This suite can be expanded to simulate the user setting non-default values for all of + // the form controls and asserting that the correct payload is sent to the API. + describe('include aliases', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot()); + httpRequestsMockHelpers.setRestoreSnapshotResponse({}); + + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('is sent to the API', async () => { + const { actions } = testBed; + actions.toggleIncludeAliases(); + actions.goToStep(3); + await actions.clickRestore(); + + const expectedPayload = { includeAliases: false }; + const latestRequest = server.requests[server.requests.length - 1]; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expectedPayload); + }); + }); }); diff --git a/x-pack/plugins/snapshot_restore/common/lib/index.ts b/x-pack/plugins/snapshot_restore/common/lib/index.ts index a375709cee7c5..fc8015c5b807b 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/index.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/index.ts @@ -6,10 +6,7 @@ */ export { flatten } from './flatten'; -export { - deserializeRestoreSettings, - serializeRestoreSettings, -} from './restore_settings_serialization'; +export { serializeRestoreSettings } from './restore_settings_serialization'; export { deserializeSnapshotDetails, deserializeSnapshotConfig, diff --git a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts index bb640000cc89a..3a78001c742ff 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { - deserializeRestoreSettings, - serializeRestoreSettings, -} from './restore_settings_serialization'; +import { serializeRestoreSettings } from './restore_settings_serialization'; describe('restore_settings_serialization()', () => { it('should serialize blank restore settings', () => { @@ -56,6 +53,7 @@ describe('restore_settings_serialization()', () => { indexSettings: '{"modified_setting":123}', ignoreIndexSettings: ['setting1'], ignoreUnavailable: true, + includeAliases: true, }) ).toEqual({ indices: ['foo', 'bar'], @@ -66,6 +64,7 @@ describe('restore_settings_serialization()', () => { index_settings: { modified_setting: 123 }, ignore_index_settings: ['setting1'], ignore_unavailable: true, + include_aliases: true, }); }); @@ -76,47 +75,4 @@ describe('restore_settings_serialization()', () => { }) ).toEqual({}); }); - - it('should deserialize blank restore settings', () => { - expect(deserializeRestoreSettings({})).toEqual({}); - }); - - it('should deserialize partial restore settings', () => { - expect(deserializeRestoreSettings({})).toEqual({}); - expect( - deserializeRestoreSettings({ - indices: ['foo', 'bar'], - ignore_index_settings: ['setting1'], - partial: true, - }) - ).toEqual({ - indices: ['foo', 'bar'], - ignoreIndexSettings: ['setting1'], - partial: true, - }); - }); - - it('should deserialize full restore settings', () => { - expect( - deserializeRestoreSettings({ - indices: ['foo', 'bar'], - rename_pattern: 'capture_pattern', - rename_replacement: 'replacement_pattern', - include_global_state: true, - partial: true, - index_settings: { modified_setting: 123 }, - ignore_index_settings: ['setting1'], - ignore_unavailable: true, - }) - ).toEqual({ - indices: ['foo', 'bar'], - renamePattern: 'capture_pattern', - renameReplacement: 'replacement_pattern', - includeGlobalState: true, - partial: true, - indexSettings: '{"modified_setting":123}', - ignoreIndexSettings: ['setting1'], - ignoreUnavailable: true, - }); - }); }); diff --git a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts index 5e026246c77b9..c017bc721884c 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts @@ -26,6 +26,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest indexSettings, ignoreIndexSettings, ignoreUnavailable, + includeAliases, } = restoreSettings; let parsedIndexSettings: RestoreSettingsEs['index_settings'] | undefined; @@ -47,32 +48,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest index_settings: parsedIndexSettings, ignore_index_settings: ignoreIndexSettings, ignore_unavailable: ignoreUnavailable, - }; - - return removeUndefinedSettings(settings); -} - -export function deserializeRestoreSettings(restoreSettingsEs: RestoreSettingsEs): RestoreSettings { - const { - indices, - rename_pattern: renamePattern, - rename_replacement: renameReplacement, - include_global_state: includeGlobalState, - partial, - index_settings: indexSettings, - ignore_index_settings: ignoreIndexSettings, - ignore_unavailable: ignoreUnavailable, - } = restoreSettingsEs; - - const settings: RestoreSettings = { - indices, - renamePattern, - renameReplacement, - includeGlobalState, - partial, - indexSettings: indexSettings ? JSON.stringify(indexSettings) : undefined, - ignoreIndexSettings, - ignoreUnavailable, + include_aliases: includeAliases, }; return removeUndefinedSettings(settings); diff --git a/x-pack/plugins/snapshot_restore/common/types/restore.ts b/x-pack/plugins/snapshot_restore/common/types/restore.ts index 1bbd5cdd5a56c..9e9b91de1859e 100644 --- a/x-pack/plugins/snapshot_restore/common/types/restore.ts +++ b/x-pack/plugins/snapshot_restore/common/types/restore.ts @@ -14,6 +14,7 @@ export interface RestoreSettings { indexSettings?: string; ignoreIndexSettings?: string[]; ignoreUnavailable?: boolean; + includeAliases?: boolean; } export interface RestoreSettingsEs { @@ -25,6 +26,7 @@ export interface RestoreSettingsEs { index_settings?: { [key: string]: any }; ignore_index_settings?: string[]; ignore_unavailable?: boolean; + include_aliases?: boolean; } export interface SnapshotRestore { diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx index f672300db8821..82ace79f49f5d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx @@ -140,6 +140,7 @@ export const RestoreSnapshotForm: React.FunctionComponent = ({ iconType="arrowRight" onClick={() => onNext()} disabled={!validation.isValid} + data-test-subj="nextButton" > = ({ iconType="check" onClick={() => executeRestore()} isLoading={isSaving} + data-test-subj="restoreButton" > {isSaving ? ( = renameReplacement, partial, includeGlobalState, + includeAliases, } = restoreSettings; // States for choosing all indices, or a subset, including caching previously chosen subset list @@ -625,6 +626,41 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = /> + + {/* Include aliases */} + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={includeAliases === undefined ? true : includeAliases} + onChange={(e) => updateRestoreSettings({ includeAliases: e.target.checked })} + data-test-subj="includeAliasesSwitch" + /> + +
); }; diff --git a/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts b/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts deleted file mode 100644 index e9244937e48c7..0000000000000 --- a/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.sr = components.clientAction.namespaceFactory(); - const sr = Client.prototype.sr.prototype; - - sr.policies = ca({ - urls: [ - { - fmt: '/_slm/policy', - }, - ], - method: 'GET', - }); - - sr.policy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - sr.deletePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - sr.executePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>/_execute', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - sr.updatePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - sr.executeRetention = ca({ - urls: [ - { - fmt: '/_slm/_execute_retention', - }, - ], - method: 'POST', - }); - - sr.cleanupRepository = ca({ - urls: [ - { - fmt: '/_snapshot/<%=name%>/_cleanup', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts b/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts index 5eb669b32e082..2c3ebf2e0176e 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts @@ -5,17 +5,24 @@ * 2.0. */ +import type { ElasticsearchClient } from 'src/core/server'; + // Cloud has its own system for managing SLM policies and we want to make // this clear when Snapshot and Restore is used in a Cloud deployment. // Retrieve the Cloud-managed policies so that UI can switch // logical paths based on this information. -export const getManagedPolicyNames = async (callWithInternalUser: any): Promise => { +export const getManagedPolicyNames = async ( + clusterClient: ElasticsearchClient +): Promise => { try { - const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', { - filterPath: '*.*managed_policies', - flatSettings: true, - includeDefaults: true, + const { + body: { persistent, transient, defaults }, + } = await clusterClient.cluster.getSettings({ + filter_path: '*.*managed_policies', + flat_settings: true, + include_defaults: true, }); + const { 'cluster.metadata.managed_policies': managedPolicyNames = [] } = { ...defaults, ...persistent, diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts b/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts index 24960120fa3be..65dc4a750c57a 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts @@ -5,18 +5,22 @@ * 2.0. */ +import type { ElasticsearchClient } from 'src/core/server'; + // Cloud has its own system for managing snapshots and we want to make // this clear when Snapshot and Restore is used in a Cloud deployment. // Retrieve the Cloud-managed repository name so that UI can switch // logical paths based on this information. export const getManagedRepositoryName = async ( - callWithInternalUser: any + client: ElasticsearchClient ): Promise => { try { - const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', { - filterPath: '*.*managed_repository', - flatSettings: true, - includeDefaults: true, + const { + body: { persistent, transient, defaults }, + } = await client.cluster.getSettings({ + filter_path: '*.*managed_repository', + flat_settings: true, + include_defaults: true, }); const { 'cluster.metadata.managed_repository': managedRepositoryName = undefined } = { ...defaults, diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index c93b5dbc4c36d..4414e3735959b 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -6,34 +6,20 @@ */ import { i18n } from '@kbn/i18n'; -import { - CoreSetup, - ILegacyCustomClusterClient, - Plugin, - Logger, - PluginInitializerContext, -} from 'kibana/server'; +import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'kibana/server'; import { PLUGIN, APP_REQUIRED_CLUSTER_PRIVILEGES } from '../common'; import { License } from './services'; import { ApiRoutes } from './routes'; import { wrapEsError } from './lib'; -import { isEsError } from './shared_imports'; -import { elasticsearchJsPlugin } from './client/elasticsearch_sr'; -import type { Dependencies, SnapshotRestoreRequestHandlerContext } from './types'; +import { handleEsError } from './shared_imports'; +import type { Dependencies } from './types'; import { SnapshotRestoreConfig } from './config'; -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - return core.elasticsearch.legacy.createClient('snapshotRestore', esClientConfig); -} - export class SnapshotRestoreServerPlugin implements Plugin { private readonly logger: Logger; private readonly apiRoutes: ApiRoutes; private readonly license: License; - private snapshotRestoreESClient?: ILegacyCustomClusterClient; constructor(private context: PluginInitializerContext) { const { logger } = this.context; @@ -52,7 +38,7 @@ export class SnapshotRestoreServerPlugin implements Plugin return; } - const router = http.createRouter(); + const router = http.createRouter(); this.license.setup( { @@ -82,17 +68,6 @@ export class SnapshotRestoreServerPlugin implements Plugin ], }); - http.registerRouteHandlerContext( - 'snapshotRestore', - async (ctx, request) => { - this.snapshotRestoreESClient = - this.snapshotRestoreESClient ?? (await getCustomEsClient(getStartServices)); - return { - client: this.snapshotRestoreESClient.asScoped(request), - }; - } - ); - this.apiRoutes.setup({ router, license: this.license, @@ -102,17 +77,11 @@ export class SnapshotRestoreServerPlugin implements Plugin isSlmEnabled: pluginConfig.slm_ui.enabled, }, lib: { - isEsError, + handleEsError, wrapEsError, }, }); } public start() {} - - public stop() { - if (this.snapshotRestoreESClient) { - this.snapshotRestoreESClient.close(); - } - } } diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts index c9ee33c1d387b..217bce9721f63 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts @@ -27,12 +27,12 @@ export function registerAppRoutes({ router, config: { isSecurityEnabled }, license, - lib: { isEsError }, + lib: { handleEsError }, }: RouteDependencies) { router.get( { path: addBasePath('privileges'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const privilegesResult: Privileges = { hasAllPrivileges: true, @@ -48,42 +48,36 @@ export function registerAppRoutes({ } try { - // Get cluster priviliges - const { has_all_requested: hasAllPrivileges, cluster } = await callAsCurrentUser( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES], - }, - } - ); + // Get cluster privileges + const { + body: { has_all_requested: hasAllPrivileges, cluster }, + } = await clusterClient.asCurrentUser.security.hasPrivileges({ + body: { + cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES], + }, + }); // Find missing cluster privileges and set overall app privileges privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); privilegesResult.hasAllPrivileges = hasAllPrivileges; // Get all index privileges the user has - const { indices } = await callAsCurrentUser('transport.request', { - path: '/_security/user/_privileges', - method: 'GET', - }); + const { + body: { indices }, + } = await clusterClient.asCurrentUser.security.getUserPrivileges(); // Check if they have all the required index privileges for at least one index - const oneIndexWithAllPrivileges = indices.find( - ({ privileges }: { privileges: string[] }) => { - if (privileges.includes('all')) { - return true; - } + const oneIndexWithAllPrivileges = indices.find(({ privileges }) => { + if (privileges.includes('all')) { + return true; + } - const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) => - privileges.includes(privilege) - ); + const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) => + privileges.includes(privilege) + ); - return indexHasAllPrivileges; - } - ); + return indexHasAllPrivileges; + }); // If they don't, return list of required index privileges if (!oneIndexWithAllPrivileges) { @@ -92,14 +86,7 @@ export function registerAppRoutes({ return res.ok({ body: privilegesResult }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts index ff66e020d2224..5ef5f2d01b96c 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -44,12 +44,23 @@ describe('[Snapshot and Restore API Routes] Policy', () => { isManagedPolicy: false, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const putClusterSettingsFn = router.getMockApiFn('cluster.putSettings'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const putLifecycleFn = router.getMockApiFn('slm.putLifecycle'); + const executeLifecycleFn = router.getMockApiFn('slm.executeLifecycle'); + const deleteLifecycleFn = router.getMockApiFn('slm.deleteLifecycle'); + const resolveIndicesFn = router.getMockApiFn('indices.resolveIndex'); beforeAll(() => { registerPolicyRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -64,7 +75,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { fooPolicy: mockEsPolicy, barPolicy: mockEsPolicy, }; - router.callAsCurrentUserResponses = [[], mockEsResponse]; + getClusterSettingsFn.mockResolvedValue({ body: {} }); + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { policies: [ { @@ -84,7 +96,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return empty array if no repositories returned from ES', async () => { const mockEsResponse = {}; - router.callAsCurrentUserResponses = [[], mockEsResponse]; + getClusterSettingsFn.mockResolvedValue({ body: {} }); + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { policies: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse, @@ -92,11 +105,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error()), // Get managed policyNames will silently fail - jest.fn().mockRejectedValueOnce(new Error()), // Call to 'sr.policies' - ]; - + getClusterSettingsFn.mockRejectedValue(new Error()); // Get managed policyNames should silently fail + getLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -116,7 +126,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { [name]: mockEsPolicy, }; - router.callAsCurrentUserResponses = [mockEsResponse, {}]; + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); + getClusterSettingsFn.mockResolvedValue({ body: {} }); const expectedResponse = { policy: { @@ -130,14 +141,20 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should return 404 error if not returned from ES', async () => { - router.callAsCurrentUserResponses = [{}, {}]; + getLifecycleFn.mockRejectedValue({ + name: 'ResponseError', + body: {}, + statusCode: 404, + }); + getClusterSettingsFn.mockResolvedValue({}); const response = await router.runRequest(mockRequest); - expect(response.status).toBe(404); + expect(response.statusCode).toBe(404); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockRejectedValueOnce(new Error('something unexpected')); + getClusterSettingsFn.mockResolvedValueOnce({ body: {} }); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -158,7 +175,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { const mockEsResponse = { snapshot_name: 'foo-policy-snapshot', }; - router.callAsCurrentUserResponses = [mockEsResponse]; + executeLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { snapshotName: 'foo-policy-snapshot', @@ -170,7 +187,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + executeLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -189,7 +206,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: names, errors: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -202,10 +219,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteLifecycleFn.mockRejectedValue(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -228,10 +242,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteLifecycleFn.mockRejectedValueOnce(mockEsError); + deleteLifecycleFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [names[1]], @@ -264,7 +276,9 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{}, mockEsResponse]; + + getLifecycleFn.mockResolvedValue({ body: {} }); + putLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -274,14 +288,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return error if policy with the same name already exists', async () => { const mockEsResponse = { [name]: {} }; - router.callAsCurrentUserResponses = [mockEsResponse]; + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const response = await router.runRequest(mockRequest); expect(response.status).toBe(409); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockResolvedValue({ body: {} }); + putLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -302,14 +317,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse]; + getLifecycleFn.mockResolvedValue({ body: { [name]: {} } }); + putLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -343,7 +359,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }, ], }; - router.callAsCurrentUserResponses = [mockEsResponse]; + + resolveIndicesFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { indices: ['fooIndex'], @@ -358,14 +375,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => { aliases: [], data_streams: [], }; - router.callAsCurrentUserResponses = [mockEsResponse]; + resolveIndicesFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { indices: [], dataStreams: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + resolveIndicesFn.mockRejectedValueOnce(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -383,14 +400,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse]; + putClusterSettingsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + putClusterSettingsFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index fa127880fd806..77264c4bffc9a 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { PutSnapshotLifecycleRequest } from '@elastic/elasticsearch/api/types'; import { schema, TypeOf } from '@kbn/config-schema'; import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types'; @@ -17,21 +17,19 @@ import { nameParameterSchema, policySchema } from './validate_schemas'; export function registerPolicyRoutes({ router, license, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all policies router.get( { path: addBasePath('policies'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; - const managedPolicies = await getManagedPolicyNames(callAsCurrentUser); + const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser); try { // Get policies - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policies', { + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ human: true, }); @@ -39,19 +37,14 @@ export function registerPolicyRoutes({ return res.ok({ body: { policies: Object.entries(policiesByName).map(([name, policy]) => { - return deserializePolicy(name, policy, managedPolicies); + // TODO: Figure out why our {@link SlmPolicyEs} is not compatible with: + // import type { SnapshotLifecyclePolicyMetadata } from '@elastic/elasticsearch/api/types'; + return deserializePolicy(name, policy as SlmPolicyEs, managedPolicies); }), }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -60,39 +53,25 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policy/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policy', { - name, + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ + policy_id: name, human: true, }); - if (!policiesByName[name]) { - // If policy doesn't exist, ES will return 200 with an empty object, so manually throw 404 here - return res.notFound({ body: 'Policy not found' }); - } - - const managedPolicies = await getManagedPolicyNames(callAsCurrentUser); + const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser); // Deserialize policy return res.ok({ body: { - policy: deserializePolicy(name, policiesByName[name], managedPolicies), + policy: deserializePolicy(name, policiesByName[name] as SlmPolicyEs, managedPolicies), }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -101,13 +80,17 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policies'), validate: { body: policySchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; + const policy = req.body as TypeOf; const { name } = policy; try { // Check that policy with the same name doesn't already exist - const policyByName = await callAsCurrentUser('sr.policy', { name }); + const { body: policyByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ + policy_id: name, + }); + if (policyByName[name]) { return res.conflict({ body: 'There is already a policy with that name.' }); } @@ -117,21 +100,15 @@ export function registerPolicyRoutes({ try { // Otherwise create new policy - const response = await callAsCurrentUser('sr.updatePolicy', { - name, - body: serializePolicy(policy), + const response = await clusterClient.asCurrentUser.slm.putLifecycle({ + policy_id: name, + // TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']} + body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -143,31 +120,25 @@ export function registerPolicyRoutes({ validate: { params: nameParameterSchema, body: policySchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const policy = req.body as TypeOf; try { // Check that policy with the given name exists // If it doesn't exist, 404 will be thrown by ES and will be returned - await callAsCurrentUser('sr.policy', { name }); + await clusterClient.asCurrentUser.slm.getLifecycle({ policy_id: name }); // Otherwise update policy - const response = await callAsCurrentUser('sr.updatePolicy', { - name, - body: serializePolicy(policy), + const response = await clusterClient.asCurrentUser.slm.putLifecycle({ + policy_id: name, + // TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']} + body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -176,7 +147,7 @@ export function registerPolicyRoutes({ router.delete( { path: addBasePath('policies/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const policyNames = name.split(','); @@ -187,7 +158,8 @@ export function registerPolicyRoutes({ await Promise.all( policyNames.map((policyName) => { - return callAsCurrentUser('sr.deletePolicy', { name: policyName }) + return clusterClient.asCurrentUser.slm + .deleteLifecycle({ policy_id: policyName }) .then(() => response.itemsDeleted.push(policyName)) .catch((e) => response.errors.push({ @@ -206,23 +178,18 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policy/{name}/run'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const { snapshot_name: snapshotName } = await callAsCurrentUser('sr.executePolicy', { - name, + const { + body: { snapshot_name: snapshotName }, + } = await clusterClient.asCurrentUser.slm.executeLifecycle({ + policy_id: name, }); return res.ok({ body: { snapshotName } }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -231,19 +198,14 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policies/indices'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; try { - const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/*`, - query: { - expand_wildcards: 'all', - }, - } - ); + const response = await clusterClient.asCurrentUser.indices.resolveIndex({ + name: '*', + expand_wildcards: 'all', + }); + const resolvedIndicesResponse = response.body as ResolveIndexResponseFromES; const body: PolicyIndicesResponse = { dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(), @@ -256,14 +218,7 @@ export function registerPolicyRoutes({ body, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -272,18 +227,21 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policies/retention_settings'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const { persistent, transient, defaults } = await callAsCurrentUser('cluster.getSettings', { - filterPath: '**.slm.retention*', - includeDefaults: true, + const { client: clusterClient } = ctx.core.elasticsearch; + const { + body: { persistent, transient, defaults }, + } = await clusterClient.asCurrentUser.cluster.getSettings({ + filter_path: '**.slm.retention*', + include_defaults: true, }); - const { slm: retentionSettings = undefined } = { + const { slm: retentionSettings }: { slm?: { retention_schedule: string } } = { ...defaults, ...persistent, ...transient, }; - const { retention_schedule: retentionSchedule } = retentionSettings; + const retentionSchedule = + retentionSettings != null ? retentionSettings.retention_schedule : undefined; return res.ok({ body: { retentionSchedule }, @@ -300,11 +258,11 @@ export function registerPolicyRoutes({ validate: { body: retentionSettingsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { retentionSchedule } = req.body as TypeOf; try { - const response = await callAsCurrentUser('cluster.putSettings', { + const response = await clusterClient.asCurrentUser.cluster.putSettings({ body: { persistent: { slm: { @@ -314,16 +272,9 @@ export function registerPolicyRoutes({ }, }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -332,9 +283,9 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policies/retention'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const response = await callAsCurrentUser('sr.executeRetention'); - return res.ok({ body: response }); + const { client: clusterClient } = ctx.core.elasticsearch; + const response = await clusterClient.asCurrentUser.slm.executeRetention(); + return res.ok({ body: response.body }); }) ); } diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts index 35dce2c5d558f..7d14d62bfe1a0 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -19,12 +19,25 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const clusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const createRepoFn = router.getMockApiFn('snapshot.createRepository'); + const getRepoFn = router.getMockApiFn('snapshot.getRepository'); + const deleteRepoFn = router.getMockApiFn('snapshot.deleteRepository'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const getSnapshotFn = router.getMockApiFn('snapshot.get'); + const verifyRepoFn = router.getMockApiFn('snapshot.verifyRepository'); + const catPluginsFn = router.getMockApiFn('cat.plugins'); beforeAll(() => { registerRepositoriesRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -48,11 +61,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockRepositoryEsResponse, - mockPolicyEsResponse, - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse }); const expectedResponse = { repositories: [ @@ -85,11 +96,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockRepositoryEsResponse, - mockPolicyEsResponse, - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse }); const expectedResponse = { repositories: [], @@ -103,10 +112,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - jest.fn().mockRejectedValueOnce(new Error()), - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -128,11 +135,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { [name]: { type: '', settings: {} }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - {}, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: {} }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -144,7 +149,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should return empty repository object if not returned from ES', async () => { - router.callAsCurrentUserResponses = [mockSnapshotGetManagedRepositoryEsResponse, {}, {}]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: {} }); + getSnapshotFn.mockResolvedValue({ body: {} }); const expectedResponse = { repository: {}, @@ -167,11 +174,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - mockEsSnapshotResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotResponse }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -190,11 +195,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }; const mockEsSnapshotError = jest.fn().mockRejectedValueOnce(new Error('snapshot error')); - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - mockEsSnapshotError, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotError }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -208,10 +211,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - jest.fn().mockRejectedValueOnce(new Error()), - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -230,7 +232,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return repository verification response if returned from ES', async () => { const mockEsResponse = { nodes: {} }; - router.callAsCurrentUserResponses = [mockEsResponse]; + verifyRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { verification: { valid: true, response: mockEsResponse }, @@ -241,7 +243,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return repository verification error if returned from ES', async () => { const mockEsResponse = { error: {}, status: 500 }; - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(mockEsResponse)]; + verifyRepoFn.mockRejectedValueOnce(mockEsResponse); const expectedResponse = { verification: { valid: false, error: mockEsResponse }, @@ -258,7 +260,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }; it('should return default types if no repository plugins returned from ES', async () => { - router.callAsCurrentUserResponses = [{}]; + catPluginsFn.mockResolvedValue({ body: {} }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES]; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -269,7 +271,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { const pluginTypes = Object.entries(REPOSITORY_PLUGINS_MAP).map(([key, value]) => value); const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))]; - router.callAsCurrentUserResponses = [mockEsResponse]; + catPluginsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES, ...pluginTypes]; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -278,7 +280,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should not return non-repository plugins returned from ES', async () => { const pluginNames = ['foo-plugin', 'bar-plugin']; const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))]; - router.callAsCurrentUserResponses = [mockEsResponse]; + catPluginsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES]; @@ -286,11 +288,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error('Error getting pluggins')), - ]; + catPluginsFn.mockRejectedValueOnce(new Error('Error getting plugins')); - await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting pluggins'); + await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting plugins'); }); }); @@ -307,7 +307,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{}, mockEsResponse]; + getRepoFn.mockResolvedValue({ body: {} }); + createRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; @@ -315,15 +316,15 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should return error if repository with the same name already exists', async () => { - router.callAsCurrentUserResponses = [{ [name]: {} }]; - + getRepoFn.mockResolvedValue({ body: { [name]: {} } }); const response = await router.runRequest(mockRequest); expect(response.status).toBe(409); }); it('should throw if ES error', async () => { const error = new Error('Oh no!'); - router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(error)]; + getRepoFn.mockResolvedValue({ body: {} }); + createRepoFn.mockRejectedValue(error); await expect(router.runRequest(mockRequest)).rejects.toThrowError(error); }); @@ -344,7 +345,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse]; + getRepoFn.mockResolvedValue({ body: { [name]: {} } }); + createRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = mockEsResponse; @@ -352,7 +354,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -369,7 +371,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse }); + deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: names, errors: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -380,10 +383,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteRepoFn.mockRejectedValueOnce(mockEsError); + deleteRepoFn.mockRejectedValueOnce(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -402,11 +403,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; + const responses = [Promise.reject(mockEsError), Promise.resolve({ body: mockEsResponse })]; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteRepoFn.mockImplementation(() => responses.shift()); const expectedResponse = { itemsDeleted: [names[1]], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index c9945bb172e6c..96099e3fbb1eb 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -6,9 +6,10 @@ */ import { TypeOf } from '@kbn/config-schema'; +import type { SnapshotRepositorySettings } from '@elastic/elasticsearch/api/types'; import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants'; -import { Repository, RepositoryType, SlmPolicyEs } from '../../../common/types'; +import { Repository, RepositoryType } from '../../../common/types'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; import { nameParameterSchema, repositorySchema } from './validate_schemas'; @@ -28,21 +29,23 @@ export function registerRepositoriesRoutes({ router, license, config: { isCloudEnabled }, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all repositories router.get( { path: addBasePath('repositories'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const managedRepositoryName = await getManagedRepositoryName(callAsCurrentUser); + const { client: clusterClient } = ctx.core.elasticsearch; + const managedRepositoryName = await getManagedRepositoryName(clusterClient.asCurrentUser); let repositoryNames: string[] | undefined; let repositories: Repository[]; let managedRepository: ManagedRepository; try { - const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', { + const { + body: repositoriesByName, + } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: '_all', }); repositoryNames = Object.keys(repositoriesByName); @@ -52,29 +55,20 @@ export function registerRepositoriesRoutes({ name, type, settings: deserializeRepositorySettings(settings), - }; + } as Repository; }); managedRepository = { name: managedRepositoryName, }; } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } // If a managed repository, we also need to check if a policy is associated to it if (managedRepositoryName) { try { - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policies', { + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ human: true, }); @@ -102,45 +96,28 @@ export function registerRepositoriesRoutes({ router.get( { path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); let repositoryByName: any; try { - repositoryByName = await callAsCurrentUser('snapshot.getRepository', { + ({ body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: name, - }); + })); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } - const { - responses: snapshotResponses, - }: { - responses: Array<{ - repository: string; - snapshots: any[]; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository: name, snapshot: '_all', - }).catch((e) => ({ - responses: [ - { - snapshots: null, - }, - ], - })); + }); + + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: snapshotResponses } = response.body; if (repositoryByName[name]) { const { type = '', settings = {} } = repositoryByName[name]; @@ -176,18 +153,20 @@ export function registerRepositoriesRoutes({ router.get( { path: addBasePath('repository_types'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; // In ECE/ESS, do not enable the default types const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES]; try { // Call with internal user so that the requesting user does not need `monitoring` cluster // privilege just to see list of available repository types - const plugins: any[] = await callAsCurrentUser('cat.plugins', { format: 'json' }); + const { body: plugins } = await clusterClient.asCurrentUser.cat.plugins({ format: 'json' }); // Filter list of plugins to repository-related ones if (plugins && plugins.length) { - const pluginNames: string[] = [...new Set(plugins.map((plugin) => plugin.component))]; + const pluginNames: string[] = [ + ...new Set(plugins.map((plugin) => plugin.component ?? '')), + ]; pluginNames.forEach((pluginName) => { if (REPOSITORY_PLUGINS_MAP[pluginName]) { types.push(REPOSITORY_PLUGINS_MAP[pluginName]); @@ -196,14 +175,7 @@ export function registerRepositoriesRoutes({ } return res.ok({ body: types }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -215,20 +187,24 @@ export function registerRepositoriesRoutes({ validate: { params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const verificationResults = await callAsCurrentUser('snapshot.verifyRepository', { - repository: name, - }).catch((e) => ({ - valid: false, - error: e.response ? JSON.parse(e.response) : e, - })); + const { body: verificationResults } = await clusterClient.asCurrentUser.snapshot + .verifyRepository({ + repository: name, + }) + .catch((e) => ({ + body: { + valid: false, + error: e.response ? JSON.parse(e.response) : e, + }, + })); return res.ok({ body: { - verification: verificationResults.error + verification: (verificationResults as { error?: Error }).error ? verificationResults : { valid: true, @@ -237,14 +213,7 @@ export function registerRepositoriesRoutes({ }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -256,20 +225,24 @@ export function registerRepositoriesRoutes({ validate: { params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const cleanupResults = await callAsCurrentUser('sr.cleanupRepository', { - name, - }).catch((e) => ({ - cleaned: false, - error: e.response ? JSON.parse(e.response) : e, - })); + const { body: cleanupResults } = await clusterClient.asCurrentUser.snapshot + .cleanupRepository({ + repository: name, + }) + .catch((e) => ({ + body: { + cleaned: false, + error: e.response ? JSON.parse(e.response) : e, + }, + })); return res.ok({ body: { - cleanup: cleanupResults.error + cleanup: (cleanupResults as { error?: Error }).error ? cleanupResults : { cleaned: true, @@ -278,14 +251,7 @@ export function registerRepositoriesRoutes({ }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -294,14 +260,16 @@ export function registerRepositoriesRoutes({ router.put( { path: addBasePath('repositories'), validate: { body: repositorySchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name = '', type = '', settings = {} } = req.body as TypeOf; // Check that repository with the same name doesn't already exist try { - const repositoryByName = await callAsCurrentUser('snapshot.getRepository', { - repository: name, - }); + const { body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository( + { + repository: name, + } + ); if (repositoryByName[name]) { return res.conflict({ body: 'There is already a repository with that name.' }); } @@ -311,25 +279,19 @@ export function registerRepositoriesRoutes({ // Otherwise create new repository try { - const response = await callAsCurrentUser('snapshot.createRepository', { + const response = await clusterClient.asCurrentUser.snapshot.createRepository({ repository: name, body: { type, - settings: serializeRepositorySettings(settings), + // TODO: Bring {@link RepositorySettings} in line with {@link SnapshotRepositorySettings} + settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings, }, verify: false, }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -341,37 +303,30 @@ export function registerRepositoriesRoutes({ validate: { body: repositorySchema, params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const { type = '', settings = {} } = req.body as TypeOf; try { // Check that repository with the given name exists // If it doesn't exist, 404 will be thrown by ES and will be returned - await callAsCurrentUser('snapshot.getRepository', { repository: name }); + await clusterClient.asCurrentUser.snapshot.getRepository({ repository: name }); // Otherwise update repository - const response = await callAsCurrentUser('snapshot.createRepository', { + const response = await clusterClient.asCurrentUser.snapshot.createRepository({ repository: name, body: { type, - settings: serializeRepositorySettings(settings), + settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings, }, verify: false, }); return res.ok({ - body: response, + body: response.body, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -380,7 +335,7 @@ export function registerRepositoriesRoutes({ router.delete( { path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const repositoryNames = name.split(','); @@ -392,7 +347,8 @@ export function registerRepositoriesRoutes({ try { await Promise.all( repositoryNames.map((repoName) => { - return callAsCurrentUser('snapshot.deleteRepository', { repository: repoName }) + return clusterClient.asCurrentUser.snapshot + .deleteRepository({ repository: repoName }) .then(() => response.itemsDeleted.push(repoName)) .catch((e) => response.errors.push({ @@ -405,14 +361,7 @@ export function registerRepositoriesRoutes({ return res.ok({ body: response }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts index fe33331522daa..a6f6924aaae31 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts @@ -17,15 +17,21 @@ describe('[Snapshot and Restore API Routes] Restore', () => { index: { size: {}, files: {} }, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); beforeAll(() => { registerRestoreRoutes({ - router: router as any, ...routeDependencies, + router, }); }); + /** + * ES APIs used by these endpoints + */ + const indicesRecoveryFn = router.getMockApiFn('indices.recovery'); + const restoreSnapshotFn = router.getMockApiFn('snapshot.restore'); + describe('Restore snapshot', () => { const mockRequest: RequestMock = { method: 'post', @@ -39,7 +45,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should return successful response from ES', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse]; + restoreSnapshotFn.mockResolvedValue({ body: mockEsResponse }); await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: mockEsResponse, @@ -47,7 +53,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + restoreSnapshotFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -76,7 +82,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }, }; - router.callAsCurrentUserResponses = [mockEsResponse]; + indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [ { @@ -100,7 +106,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should return empty array if no repositories returned from ES', async () => { const mockEsResponse = {}; - router.callAsCurrentUserResponses = [mockEsResponse]; + indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse: any[] = []; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -109,7 +115,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + indicesRecoveryFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts index c4300bafc75fb..b7281fee04c53 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts @@ -6,6 +6,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { RestoreRequest } from '@elastic/elasticsearch/api/types'; import { SnapshotRestore, SnapshotRestoreShardEs } from '../../../common/types'; import { serializeRestoreSettings } from '../../../common/lib'; @@ -14,20 +15,20 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; import { restoreSettingsSchema } from './validate_schemas'; -export function registerRestoreRoutes({ router, license, lib: { isEsError } }: RouteDependencies) { +export function registerRestoreRoutes({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { // GET all snapshot restores router.get( { path: addBasePath('restores'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; try { const snapshotRestores: SnapshotRestore[] = []; - const recoveryByIndexName: { - [key: string]: { - shards: SnapshotRestoreShardEs[]; - }; - } = await callAsCurrentUser('indices.recovery', { + const { body: recoveryByIndexName } = await clusterClient.asCurrentUser.indices.recovery({ human: true, }); @@ -40,7 +41,8 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R .filter((shard) => shard.type === 'SNAPSHOT') .sort((a, b) => a.id - b.id) .map((shard) => { - const deserializedShard = deserializeRestoreShard(shard); + // TODO: Bring {@link SnapshotRestoreShardEs} in line with {@link ShardRecovery} + const deserializedShard = deserializeRestoreShard(shard as SnapshotRestoreShardEs); const { startTimeInMillis, stopTimeInMillis } = deserializedShard; // Set overall latest activity time @@ -80,14 +82,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R return res.ok({ body: snapshotRestores }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -104,27 +99,21 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R validate: { body: restoreSettingsSchema, params: restoreParamsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { repository, snapshot } = req.params as TypeOf; const restoreSettings = req.body as TypeOf; try { - const response = await callAsCurrentUser('snapshot.restore', { + const response = await clusterClient.asCurrentUser.snapshot.restore({ repository, snapshot, - body: serializeRestoreSettings(restoreSettings), + // TODO: Bring {@link RestoreSettingsEs} in line with {@link RestoreRequest['body']} + body: serializeRestoreSettings(restoreSettings) as RestoreRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index 97eb34b4aaa73..bd7dffe987feb 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -29,12 +29,21 @@ const defaultSnapshot = { }; describe('[Snapshot and Restore API Routes] Snapshots', () => { - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const getRepoFn = router.getMockApiFn('snapshot.getRepository'); + const getSnapshotFn = router.getMockApiFn('snapshot.get'); + const deleteSnapshotFn = router.getMockApiFn('snapshot.delete'); beforeAll(() => { registerSnapshotsRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -60,31 +69,29 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { barRepository: {}, }; - const mockGetSnapshotsFooResponse = Promise.resolve({ + const mockGetSnapshotsFooResponse = { responses: [ { repository: 'fooRepository', snapshots: [{ snapshot: 'snapshot1' }], }, ], - }); + }; - const mockGetSnapshotsBarResponse = Promise.resolve({ + const mockGetSnapshotsBarResponse = { responses: [ { repository: 'barRepository', snapshots: [{ snapshot: 'snapshot2' }], }, ], - }); + }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetPolicyEsResponse, - mockSnapshotGetRepositoryEsResponse, - mockGetSnapshotsFooResponse, - mockGetSnapshotsBarResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse }); + getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsFooResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsBarResponse }); const expectedResponse = { errors: {}, @@ -120,11 +127,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { const mockSnapshotGetPolicyEsResponse = {}; const mockSnapshotGetRepositoryEsResponse = {}; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetPolicyEsResponse, - mockSnapshotGetRepositoryEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse }); + getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse }); const expectedResponse = { errors: [], @@ -138,11 +143,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { }); test('throws if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error('Error getting managed repository')), - jest.fn().mockRejectedValueOnce(new Error('Error getting policies')), - jest.fn().mockRejectedValueOnce(new Error('Error getting repository')), - ]; + getClusterSettingsFn.mockRejectedValueOnce(new Error()); + getLifecycleFn.mockRejectedValueOnce(new Error()); + getRepoFn.mockRejectedValueOnce(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -177,10 +180,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse }); const expectedResponse = { ...defaultSnapshot, @@ -215,12 +216,13 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse }); - await expect(router.runRequest(mockRequest)).rejects.toThrowError(); + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: 'Snapshot not found', + status: 404, + }); }); }); @@ -243,7 +245,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [ @@ -261,10 +264,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -289,10 +290,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [{ snapshot: 'snapshot-2', repository: 'barRepository' }], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 03e3b4ecc0887..8f6f44f63a556 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -6,31 +6,31 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { RouteDependencies } from '../../types'; -import { addBasePath } from '../helpers'; -import { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types'; +import type { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types'; import { deserializeSnapshotDetails } from '../../../common/lib'; +import type { RouteDependencies } from '../../types'; import { getManagedRepositoryName } from '../../lib'; +import { addBasePath } from '../helpers'; export function registerSnapshotsRoutes({ router, license, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all snapshots router.get( { path: addBasePath('snapshots'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); let policies: string[] = []; // Attempt to retrieve policies // This could fail if user doesn't have access to read SLM policies try { - const policiesByName = await callAsCurrentUser('sr.policies'); + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle(); policies = Object.keys(policiesByName); } catch (e) { // Silently swallow error as policy names aren't required in UI @@ -44,7 +44,9 @@ export function registerSnapshotsRoutes({ let repositoryNames: string[]; try { - const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', { + const { + body: repositoriesByName, + } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: '_all', }); repositoryNames = Object.keys(repositoriesByName); @@ -55,13 +57,7 @@ export function registerSnapshotsRoutes({ }); } } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - throw e; + return handleEsError({ error: e, response: res }); } const snapshots: SnapshotDetails[] = []; @@ -71,23 +67,27 @@ export function registerSnapshotsRoutes({ const fetchSnapshotsForRepository = async (repository: string) => { try { // If any of these repositories 504 they will cost the request significant time. - const { - responses: fetchedResponses, - }: { - responses: Array<{ - repository: 'string'; - snapshots: SnapshotDetailsEs[]; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository, snapshot: '_all', ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. }); + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: fetchedResponses } = response.body; + // Decorate each snapshot with the repository with which it's associated. + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client fetchedResponses.forEach(({ snapshots: fetchedSnapshots }) => { + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client fetchedSnapshots.forEach((snapshot) => { - snapshots.push(deserializeSnapshotDetails(repository, snapshot, managedRepository)); + snapshots.push( + deserializeSnapshotDetails( + repository, + snapshot as SnapshotDetailsEs, + managedRepository + ) + ); }); }); @@ -124,28 +124,27 @@ export function registerSnapshotsRoutes({ validate: { params: getOneParamsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { repository, snapshot } = req.params as TypeOf; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); try { - const { - responses: snapshotsResponse, - }: { - responses: Array<{ - repository: string; - snapshots: SnapshotDetailsEs[]; - error?: any; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository, snapshot: '_all', ignore_unavailable: true, }); + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: snapshotsResponse } = response.body; + const snapshotsList = snapshotsResponse && snapshotsResponse[0] && snapshotsResponse[0].snapshots; + if (!snapshotsList || snapshotsList.length === 0) { + return res.notFound({ body: 'Snapshot not found' }); + } const selectedSnapshot = snapshotsList.find( + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client ({ snapshot: snapshotName }) => snapshot === snapshotName ) as SnapshotDetailsEs; @@ -155,10 +154,12 @@ export function registerSnapshotsRoutes({ } const successfulSnapshots = snapshotsList + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client .filter(({ state }) => state === 'SUCCESS') + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client .sort((a, b) => { - return +new Date(b.end_time) - +new Date(a.end_time); - }); + return +new Date(b.end_time!) - +new Date(a.end_time!); + }) as SnapshotDetailsEs[]; return res.ok({ body: deserializeSnapshotDetails( @@ -169,14 +170,7 @@ export function registerSnapshotsRoutes({ ), }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -192,7 +186,7 @@ export function registerSnapshotsRoutes({ router.post( { path: addBasePath('snapshots/bulk_delete'), validate: { body: deleteSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const response: { itemsDeleted: Array<{ snapshot: string; repository: string }>; @@ -210,7 +204,8 @@ export function registerSnapshotsRoutes({ for (let i = 0; i < snapshots.length; i++) { const { snapshot, repository } = snapshots[i]; - await callAsCurrentUser('snapshot.delete', { snapshot, repository }) + await clusterClient.asCurrentUser.snapshot + .delete({ snapshot, repository }) .then(() => response.itemsDeleted.push({ snapshot, repository })) .catch((e) => response.errors.push({ @@ -222,14 +217,7 @@ export function registerSnapshotsRoutes({ return res.ok({ body: response }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts index fe156f6ba9750..af31466c2cefe 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts @@ -176,4 +176,5 @@ export const restoreSettingsSchema = schema.object({ indexSettings: schema.maybe(schema.string()), ignoreIndexSettings: schema.maybe(schema.arrayOf(schema.string())), ignoreUnavailable: schema.maybe(schema.boolean()), + includeAliases: schema.maybe(schema.boolean()), }); diff --git a/x-pack/plugins/snapshot_restore/server/services/license.ts b/x-pack/plugins/snapshot_restore/server/services/license.ts index 93cf86eae5359..e209edcd899b4 100644 --- a/x-pack/plugins/snapshot_restore/server/services/license.ts +++ b/x-pack/plugins/snapshot_restore/server/services/license.ts @@ -6,11 +6,15 @@ */ import { Logger } from 'src/core/server'; -import type { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'kibana/server'; +import type { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; import { LicensingPluginSetup } from '../../../licensing/server'; import { LicenseType } from '../../../licensing/common/types'; -import type { SnapshotRestoreRequestHandlerContext } from '../types'; export interface LicenseStatus { isValid: boolean; @@ -51,13 +55,11 @@ export class License { }); } - guardApiRoute( - handler: RequestHandler - ) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( - ctx: Context, + ctx: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory ) { diff --git a/x-pack/plugins/snapshot_restore/server/shared_imports.ts b/x-pack/plugins/snapshot_restore/server/shared_imports.ts index df9b3dd53cc1f..7f55d189457c7 100644 --- a/x-pack/plugins/snapshot_restore/server/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/server/shared_imports.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts b/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts index 6bd7c10497b24..77c8ab4759b53 100644 --- a/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts +++ b/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts @@ -6,13 +6,14 @@ */ import { License } from '../../services'; +import { handleEsError } from '../../shared_imports'; import { wrapEsError } from '../../lib'; -import { isEsError } from '../../shared_imports'; +import type { RouteDependencies } from '../../types'; const license = new License(); license.getStatus = jest.fn().mockReturnValue({ isValid: true }); -export const routeDependencies = { +export const routeDependencies: Omit = { license, config: { isSecurityEnabled: jest.fn().mockReturnValue(true), @@ -20,7 +21,7 @@ export const routeDependencies = { isSlmEnabled: true, }, lib: { - isEsError, wrapEsError, + handleEsError, }, }; diff --git a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts index 656301abc5358..efd0ebd0fd1c4 100644 --- a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts +++ b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { set } from '@elastic/safer-lodash-set'; +import type { IRouter } from 'src/core/server'; +import { get } from 'lodash'; + +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; type RequestHandler = (...params: any[]) => any; @@ -48,7 +51,7 @@ export interface RequestMock { [key: string]: any; } -export class RouterMock { +export class RouterMock implements IRouter { /** * Cache to keep a reference to all the request handler defined on the router for each HTTP method and path */ @@ -60,15 +63,13 @@ export class RouterMock { patch: {}, }; - private _callAsCurrentUserCallCount = 0; - private _callAsCurrentUserResponses: any[] = []; - private contextMock = {}; + public contextMock = { + core: { elasticsearch: { client: elasticsearchServiceMock.createScopedClusterClient() } }, + }; - constructor(pathToESclient = 'core.elasticsearch.dataClient') { - set(this.contextMock, pathToESclient, { - callAsCurrentUser: this.callAsCurrentUser.bind(this), - }); - } + getRoutes = jest.fn(); + handleLegacyErrors = jest.fn(); + routerPath = ''; get({ path }: { path: string }, handler: RequestHandler) { this.cacheHandlers.get[path] = handler; @@ -90,17 +91,8 @@ export class RouterMock { this.cacheHandlers.patch[path] = handler; } - private callAsCurrentUser() { - const index = this._callAsCurrentUserCallCount; - this._callAsCurrentUserCallCount += 1; - const response = this._callAsCurrentUserResponses[index]; - - return typeof response === 'function' ? Promise.resolve(response()) : Promise.resolve(response); - } - - public set callAsCurrentUserResponses(responses: any[]) { - this._callAsCurrentUserCallCount = 0; - this._callAsCurrentUserResponses = responses; + getMockApiFn(path: string): jest.Mock { + return get(this.contextMock.core.elasticsearch.client.asCurrentUser, path); } runRequest({ method, path, ...mockRequest }: RequestMock) { diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index c92de645aa2de..8c2ad74865e45 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -5,19 +5,14 @@ * 2.0. */ -import type { - LegacyScopedClusterClient, - ILegacyScopedClusterClient, - IRouter, - RequestHandlerContext, -} from 'src/core/server'; +import type { IRouter, RequestHandlerContext, IScopedClusterClient } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { CloudSetup } from '../../cloud/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; import { wrapEsError } from './lib'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; @@ -27,7 +22,7 @@ export interface Dependencies { } export interface RouteDependencies { - router: SnapshotRestoreRouter; + router: IRouter; license: License; config: { isSlmEnabled: boolean; @@ -35,8 +30,8 @@ export interface RouteDependencies { isCloudEnabled: boolean; }; lib: { - isEsError: typeof isEsError; wrapEsError: typeof wrapEsError; + handleEsError: typeof handleEsError; }; } @@ -56,13 +51,13 @@ export interface ResolveIndexResponseFromES { data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>; } -export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; +export type CallAsCurrentUser = IScopedClusterClient['asCurrentUser']; /** * @internal */ export interface SnapshotRestoreContext { - client: ILegacyScopedClusterClient; + client: IScopedClusterClient; } /** @@ -71,8 +66,3 @@ export interface SnapshotRestoreContext { export interface SnapshotRestoreRequestHandlerContext extends RequestHandlerContext { snapshotRestore: SnapshotRestoreContext; } - -/** - * @internal - */ -export type SnapshotRestoreRouter = IRouter; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index ef09937da3fbc..e1e0711c2bb2c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1837,17 +1837,35 @@ }, "agents": { "properties": { - "total": { - "type": "long" + "total_enrolled": { + "type": "long", + "_meta": { + "description": "The total number of enrolled agents, in any state" + } }, - "online": { - "type": "long" + "healthy": { + "type": "long", + "_meta": { + "description": "The total number of enrolled agents in a healthy state" + } }, - "error": { - "type": "long" + "unhealthy": { + "type": "long", + "_meta": { + "description": "The total number of enrolled agents in an unhealthy state" + } }, "offline": { - "type": "long" + "type": "long", + "_meta": { + "description": "The total number of enrolled agents currently offline" + } + }, + "total_all_statuses": { + "type": "long", + "_meta": { + "description": "The total number of agents in any state, both enrolled and inactive" + } } } }, @@ -1978,6 +1996,42 @@ "xy_layer_added": { "type": "long" }, + "open_field_editor_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + } + }, + "open_field_editor_add": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to add a field from within Lens." + } + }, + "save_field_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user edited a field from within Lens." + } + }, + "save_field_add": { + "type": "long", + "_meta": { + "description": "Number of times the user added a field from within Lens." + } + }, + "open_field_delete_modal": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the field delete modal from within Lens." + } + }, + "delete_field": { + "type": "long", + "_meta": { + "description": "Number of times the user deleted a field from within Lens." + } + }, "indexpattern_dimension_operation_terms": { "type": "long", "_meta": { @@ -2034,70 +2088,50 @@ }, "indexpattern_dimension_operation_range": { "type": "long", - "_meta": { "description": "Number of times the range function was selected" } + "_meta": { + "description": "Number of times the range function was selected" + } }, "indexpattern_dimension_operation_median": { "type": "long", - "_meta": { "description": "Number of times the median function was selected" } + "_meta": { + "description": "Number of times the median function was selected" + } }, "indexpattern_dimension_operation_percentile": { - "type": "long", - "_meta": { "description": "Number of times the percentile function was selected" } - }, - "indexpattern_dimension_operation_last_value": { - "type": "long", - "_meta": { "description": "Number of times the last value function was selected" } - }, - "indexpattern_dimension_operation_cumulative_sum": { - "type": "long", - "_meta": { "description": "Number of times the cumulative sum function was selected" } - }, - "indexpattern_dimension_operation_counter_rate": { - "type": "long", - "_meta": { "description": "Number of times the counter rate function was selected" } - }, - "indexpattern_dimension_operation_derivative": { - "type": "long", - "_meta": { "description": "Number of times the derivative function was selected" } - }, - "indexpattern_dimension_operation_moving_average": { - "type": "long", - "_meta": { "description": "Number of times the moving average function was selected" } - }, - "open_field_editor_edit": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + "description": "Number of times the percentile function was selected" } }, - "open_field_editor_add": { + "indexpattern_dimension_operation_last_value": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to add a field from within Lens." + "description": "Number of times the last value function was selected" } }, - "save_field_edit": { + "indexpattern_dimension_operation_cumulative_sum": { "type": "long", "_meta": { - "description": "Number of times the user edited a field from within Lens." + "description": "Number of times the cumulative sum function was selected" } }, - "save_field_add": { + "indexpattern_dimension_operation_counter_rate": { "type": "long", "_meta": { - "description": "Number of times the user added a field from within Lens." + "description": "Number of times the counter rate function was selected" } }, - "open_field_delete_modal": { + "indexpattern_dimension_operation_derivative": { "type": "long", "_meta": { - "description": "Number of times the user opened the field delete modal from within Lens." + "description": "Number of times the derivative function was selected" } }, - "delete_field": { + "indexpattern_dimension_operation_moving_average": { "type": "long", "_meta": { - "description": "Number of times the user deleted a field from within Lens." + "description": "Number of times the moving average function was selected" } } } @@ -2185,6 +2219,42 @@ "xy_layer_added": { "type": "long" }, + "open_field_editor_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + } + }, + "open_field_editor_add": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the editor flyout to add a field from within Lens." + } + }, + "save_field_edit": { + "type": "long", + "_meta": { + "description": "Number of times the user edited a field from within Lens." + } + }, + "save_field_add": { + "type": "long", + "_meta": { + "description": "Number of times the user added a field from within Lens." + } + }, + "open_field_delete_modal": { + "type": "long", + "_meta": { + "description": "Number of times the user opened the field delete modal from within Lens." + } + }, + "delete_field": { + "type": "long", + "_meta": { + "description": "Number of times the user deleted a field from within Lens." + } + }, "indexpattern_dimension_operation_terms": { "type": "long", "_meta": { @@ -2241,70 +2311,50 @@ }, "indexpattern_dimension_operation_range": { "type": "long", - "_meta": { "description": "Number of times the range function was selected" } + "_meta": { + "description": "Number of times the range function was selected" + } }, "indexpattern_dimension_operation_median": { "type": "long", - "_meta": { "description": "Number of times the median function was selected" } + "_meta": { + "description": "Number of times the median function was selected" + } }, "indexpattern_dimension_operation_percentile": { - "type": "long", - "_meta": { "description": "Number of times the percentile function was selected" } - }, - "indexpattern_dimension_operation_last_value": { - "type": "long", - "_meta": { "description": "Number of times the last value function was selected" } - }, - "indexpattern_dimension_operation_cumulative_sum": { - "type": "long", - "_meta": { "description": "Number of times the cumulative sum function was selected" } - }, - "indexpattern_dimension_operation_counter_rate": { - "type": "long", - "_meta": { "description": "Number of times the counter rate function was selected" } - }, - "indexpattern_dimension_operation_derivative": { - "type": "long", - "_meta": { "description": "Number of times the derivative function was selected" } - }, - "indexpattern_dimension_operation_moving_average": { - "type": "long", - "_meta": { "description": "Number of times the moving average function was selected" } - }, - "open_field_editor_edit": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to edit a field from within Lens." + "description": "Number of times the percentile function was selected" } }, - "open_field_editor_add": { + "indexpattern_dimension_operation_last_value": { "type": "long", "_meta": { - "description": "Number of times the user opened the editor flyout to add a field from within Lens." + "description": "Number of times the last value function was selected" } }, - "save_field_edit": { + "indexpattern_dimension_operation_cumulative_sum": { "type": "long", "_meta": { - "description": "Number of times the user edited a field from within Lens." + "description": "Number of times the cumulative sum function was selected" } }, - "save_field_add": { + "indexpattern_dimension_operation_counter_rate": { "type": "long", "_meta": { - "description": "Number of times the user added a field from within Lens." + "description": "Number of times the counter rate function was selected" } }, - "open_field_delete_modal": { + "indexpattern_dimension_operation_derivative": { "type": "long", "_meta": { - "description": "Number of times the user opened the field delete modal from within Lens." + "description": "Number of times the derivative function was selected" } }, - "delete_field": { + "indexpattern_dimension_operation_moving_average": { "type": "long", "_meta": { - "description": "Number of times the user deleted a field from within Lens." + "description": "Number of times the moving average function was selected" } } } diff --git a/x-pack/plugins/timelines/.eslintrc.js b/x-pack/plugins/timelines/.eslintrc.js new file mode 100644 index 0000000000000..b267018448ba6 --- /dev/null +++ b/x-pack/plugins/timelines/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], + rules: { + '@kbn/eslint/require-license-header': 'off', + }, +}; diff --git a/x-pack/plugins/timelines/.i18nrc.json b/x-pack/plugins/timelines/.i18nrc.json new file mode 100644 index 0000000000000..4fe01ccc7bc69 --- /dev/null +++ b/x-pack/plugins/timelines/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "timelines", + "paths": { + "timelines": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/x-pack/plugins/timelines/README.md b/x-pack/plugins/timelines/README.md new file mode 100644 index 0000000000000..441a505903698 --- /dev/null +++ b/x-pack/plugins/timelines/README.md @@ -0,0 +1,11 @@ +# timelines +Timelines is a plugin that provides a grid component with accompanying server side apis to help users identify events of interest and perform root cause analysis within Kibana. + + +## Using timelines in another plugin +- Add `TimelinesPluginSetup` to Kibana plugin `SetupServices` dependencies: + +```ts +timelines: TimelinesPluginSetup; +``` +- Once `timelines` is added as a required plugin in the consuming plugin's kibana.json, timeline functionality will be available as any other kibana plugin, ie PluginSetupDependencies.timelines.getTimeline() diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts new file mode 100644 index 0000000000000..2354c513f73b8 --- /dev/null +++ b/x-pack/plugins/timelines/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = 'timelines'; +export const PLUGIN_NAME = 'timelines'; diff --git a/x-pack/plugins/timelines/kibana.json b/x-pack/plugins/timelines/kibana.json new file mode 100644 index 0000000000000..552ddfd25ce73 --- /dev/null +++ b/x-pack/plugins/timelines/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "timelines", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "timelines"], + "server": true, + "ui": true, + "requiredPlugins": [], + "optionalPlugins": [] +} diff --git a/x-pack/plugins/timelines/public/components/index.tsx b/x-pack/plugins/timelines/public/components/index.tsx new file mode 100644 index 0000000000000..3388b3c44baff --- /dev/null +++ b/x-pack/plugins/timelines/public/components/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; + +import { PLUGIN_NAME } from '../../common'; +import { TimelineProps } from '../types'; + +export const Timeline = (props: TimelineProps) => { + return ( + +
+ +
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { Timeline as default }; diff --git a/x-pack/plugins/timelines/public/index.scss b/x-pack/plugins/timelines/public/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts new file mode 100644 index 0000000000000..b535def809de3 --- /dev/null +++ b/x-pack/plugins/timelines/public/index.ts @@ -0,0 +1,11 @@ +import './index.scss'; + +import { PluginInitializerContext } from 'src/core/public'; +import { TimelinesPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin(initializerContext: PluginInitializerContext) { + return new TimelinesPlugin(initializerContext); +} +export { TimelinesPluginSetup } from './types'; diff --git a/x-pack/plugins/timelines/public/methods/index.tsx b/x-pack/plugins/timelines/public/methods/index.tsx new file mode 100644 index 0000000000000..f999e14ce910c --- /dev/null +++ b/x-pack/plugins/timelines/public/methods/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { TimelineProps } from '../types'; + +export const getTimelineLazy = (props: TimelineProps) => { + const TimelineLazy = lazy(() => import('../components')); + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts new file mode 100644 index 0000000000000..7e90d9467fefd --- /dev/null +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -0,0 +1,24 @@ +import { CoreSetup, Plugin, PluginInitializerContext } from '../../../../src/core/public'; +import { TimelinesPluginSetup, TimelineProps } from './types'; +import { getTimelineLazy } from './methods'; + +export class TimelinesPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup): TimelinesPluginSetup { + const config = this.initializerContext.config.get<{ enabled: boolean }>(); + if (!config.enabled) { + return {}; + } + + return { + getTimeline: (props: TimelineProps) => { + return getTimelineLazy(props); + }, + }; + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts new file mode 100644 index 0000000000000..b199b45902718 --- /dev/null +++ b/x-pack/plugins/timelines/public/types.ts @@ -0,0 +1,9 @@ +import { ReactElement } from 'react'; + +export interface TimelinesPluginSetup { + getTimeline?: (props: TimelineProps) => ReactElement; +} + +export interface TimelineProps { + timelineId: string; +} diff --git a/x-pack/plugins/timelines/server/config.ts b/x-pack/plugins/timelines/server/config.ts new file mode 100644 index 0000000000000..633a95b8f91a7 --- /dev/null +++ b/x-pack/plugins/timelines/server/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/timelines/server/index.ts b/x-pack/plugins/timelines/server/index.ts new file mode 100644 index 0000000000000..32de97be2704a --- /dev/null +++ b/x-pack/plugins/timelines/server/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; +import { TimelinesPlugin } from './plugin'; +import { ConfigSchema } from './config'; + +export const config = { + schema: ConfigSchema, + exposeToBrowser: { + enabled: true, + }, +}; +export function plugin(initializerContext: PluginInitializerContext) { + return new TimelinesPlugin(initializerContext); +} + +export { TimelinesPluginSetup, TimelinesPluginStart } from './types'; diff --git a/x-pack/plugins/timelines/server/plugin.ts b/x-pack/plugins/timelines/server/plugin.ts new file mode 100644 index 0000000000000..3e330b19b7fdb --- /dev/null +++ b/x-pack/plugins/timelines/server/plugin.ts @@ -0,0 +1,35 @@ +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../../src/core/server'; + +import { TimelinesPluginSetup, TimelinesPluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class TimelinesPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('timelines: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('timelines: Started'); + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/timelines/server/routes/index.ts b/x-pack/plugins/timelines/server/routes/index.ts new file mode 100644 index 0000000000000..edb10c579b30b --- /dev/null +++ b/x-pack/plugins/timelines/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '../../../../../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/timeline/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/x-pack/plugins/timelines/server/types.ts b/x-pack/plugins/timelines/server/types.ts new file mode 100644 index 0000000000000..cb544562b79b4 --- /dev/null +++ b/x-pack/plugins/timelines/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TimelinesPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TimelinesPluginStart {} diff --git a/x-pack/plugins/timelines/tsconfig.json b/x-pack/plugins/timelines/tsconfig.json new file mode 100644 index 0000000000000..67e606e798c03 --- /dev/null +++ b/x-pack/plugins/timelines/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + // add all the folders contains files to be compiled + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f55b6c5217ad9..b5cd924f480f9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2097,7 +2097,7 @@ "home.tutorials.common.auditbeatStatusCheck.successText": "データを受信しました", "home.tutorials.common.auditbeatStatusCheck.text": "Auditbeat からデータを受け取ったことを確認してください。", "home.tutorials.common.auditbeatStatusCheck.title": "ステータス", - "home.tutorials.common.cloudInstructions.passwordAndResetLink": "{passwordTemplate}が「Elastic」ユーザーのパスワードです。\\{#config.cloud.resetPasswordUrl\\}\n パスワードを忘れた場合[Elastic Cloudでリセット] (\\{config.cloud.resetPasswordUrl\\}) 。\n \\{/config.cloud.resetPasswordUrl\\}", + "home.tutorials.common.cloudInstructions.passwordAndResetLink": "{passwordTemplate}が「Elastic」ユーザーのパスワードです。\\{#config.cloud.base_url\\}\\{#config.cloud.profile_url\\}\n パスワードを忘れた場合[Elastic Cloudでリセット] (\\{#config.cloud.base_url\\}\\{config.cloud.profile_url\\}) 。\n \\{#config.cloud.base_url\\}\\{/config.cloud.profile_url\\}", "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "はじめに", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "はじめに", @@ -3027,7 +3027,6 @@ "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "高度な設定", "kibana-react.tableListView.listing.listingLimitExceededDescription": "{totalItems} 件の {entityNamePlural} がありますが、{listingLimitText} の設定により {listingLimitValue} 件までしか下の表に表示できません。{advancedSettingsLink} の下でこの設定を変更できます。", "kibana-react.tableListView.listing.listingLimitExceededTitle": "リスティング制限超過", - "kibana-react.tableListView.listing.noAvailableItemsMessage": "利用可能な {entityNamePlural} がありません。", "kibana-react.tableListView.listing.noMatchedItemsMessage": "検索条件に一致する {entityNamePlural} がありません。", "kibana-react.tableListView.listing.table.actionTitle": "アクション", "kibana-react.tableListView.listing.table.editActionDescription": "編集", @@ -12579,7 +12578,6 @@ "xpack.maps.mapListing.descriptionFieldTitle": "説明", "xpack.maps.mapListing.entityName": "マップ", "xpack.maps.mapListing.entityNamePlural": "マップ", - "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "マップを読み込めません", "xpack.maps.mapListing.titleFieldTitle": "タイトル", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "インデックスパターンを選択", "xpack.maps.mapSavedObjectLabel": "マップ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c0652b8ac2a65..58119a0739812 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2108,7 +2108,7 @@ "home.tutorials.common.auditbeatStatusCheck.successText": "已成功接收数据", "home.tutorials.common.auditbeatStatusCheck.text": "确认从 Auditbeat 收到数据", "home.tutorials.common.auditbeatStatusCheck.title": "状态", - "home.tutorials.common.cloudInstructions.passwordAndResetLink": "其中 {passwordTemplate} 是用户 `elastic` 的密码。\\{#config.cloud.resetPasswordUrl\\}\n 忘了密码?[在 Elastic Cloud 中重置](\\{config.cloud.resetPasswordUrl\\})。\n \\{/config.cloud.resetPasswordUrl\\}", + "home.tutorials.common.cloudInstructions.passwordAndResetLink": "其中 {passwordTemplate} 是用户 `elastic` 的密码。\\{#config.cloud.base_url\\}\\{#config.cloud.profile_url\\}\n 忘了密码?[在 Elastic Cloud 中重置](\\{#config.cloud.base_url\\}\\{config.cloud.profile_url\\})。\n \\{#config.cloud.base_url\\}\\{/config.cloud.profile_url\\}", "home.tutorials.common.filebeat.cloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premCloudInstructions.gettingStarted.title": "入门", "home.tutorials.common.filebeat.premInstructions.gettingStarted.title": "入门", @@ -3048,7 +3048,6 @@ "kibana-react.tableListView.listing.listingLimitExceeded.advancedSettingsLinkText": "高级设置", "kibana-react.tableListView.listing.listingLimitExceededDescription": "您有 {totalItems} 个{entityNamePlural},但您的“{listingLimitText}”设置阻止下表显示 {listingLimitValue} 个以上。您可以在“{advancedSettingsLink}”下更改此设置。", "kibana-react.tableListView.listing.listingLimitExceededTitle": "已超过列表限制", - "kibana-react.tableListView.listing.noAvailableItemsMessage": "没有可用的{entityNamePlural}。", "kibana-react.tableListView.listing.noMatchedItemsMessage": "没有任何{entityNamePlural}匹配您的搜索。", "kibana-react.tableListView.listing.table.actionTitle": "操作", "kibana-react.tableListView.listing.table.editActionDescription": "编辑", @@ -12744,7 +12743,6 @@ "xpack.maps.mapListing.descriptionFieldTitle": "描述", "xpack.maps.mapListing.entityName": "地图", "xpack.maps.mapListing.entityNamePlural": "地图", - "xpack.maps.mapListing.errorAttemptingToLoadSavedMaps": "无法加载地图", "xpack.maps.mapListing.titleFieldTitle": "标题", "xpack.maps.maps.choropleth.rightSourcePlaceholder": "选择索引模式", "xpack.maps.mapSavedObjectLabel": "地图", diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx index f2da38091e37f..6706a435c7b6b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/embedded_map.tsx @@ -26,7 +26,7 @@ import { import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../maps/public'; import { MapToolTipComponent } from './map_tool_tip'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; +import type { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; export interface EmbeddedMapProps { upPoints: LocationPoint[]; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx index f2d1227fe870e..c03ed94f8c544 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx @@ -22,8 +22,7 @@ import { AppState } from '../../../../../state'; import { monitorLocationsSelector } from '../../../../../state/selectors'; import { useMonitorId } from '../../../../../hooks'; import { MonitorLocation } from '../../../../../../common/runtime_types/monitor'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderTooltipContentParams } from '../../../../../../../maps/public/classes/tooltips/tooltip_property'; +import type { RenderTooltipContentParams } from '../../../../../../../maps/public'; import { formatAvailabilityValue } from '../../availability_reporting/availability_reporting'; import { LastCheckLabel } from '../../translations'; diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index 59ce697811aa7..a8d20ff56de08 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -112,8 +112,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // Skip until https://github.com/elastic/kibana/issues/88661 gets closed - it.skip('lens XY chart with multiple layers', async () => { + it('lens XY chart with multiple layers', async () => { await PageObjects.lens.createLayer(); await PageObjects.lens.switchToVisualization('area'); diff --git a/x-pack/test/api_integration/apis/management/rollup/rollup.js b/x-pack/test/api_integration/apis/management/rollup/rollup.js index 4cb2ef6ea0fa0..a556c8071ca80 100644 --- a/x-pack/test/api_integration/apis/management/rollup/rollup.js +++ b/x-pack/test/api_integration/apis/management/rollup/rollup.js @@ -24,7 +24,8 @@ export default function ({ getService }) { cleanUp, } = registerHelpers(getService); - describe('jobs', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96002 + describe.skip('jobs', () => { after(() => cleanUp()); describe('indices', () => { diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts index 06ea5dc800e45..9b4d39a3b10b3 100644 --- a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts @@ -14,7 +14,7 @@ interface SlmPolicy { repository: string; isManagedPolicy: boolean; config?: { - indices?: string | string[]; + indices: string | string[]; ignoreUnavailable?: boolean; includeGlobalState?: boolean; partial?: boolean; @@ -36,19 +36,21 @@ interface SlmPolicy { export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { let policiesCreated: string[] = []; - const es = getService('legacyEs'); + const es = getService('es'); const createRepository = (repoName: string) => { - return es.snapshot.createRepository({ - repository: repoName, - body: { - type: 'fs', - settings: { - location: '/tmp/', + return es.snapshot + .createRepository({ + repository: repoName, + body: { + type: 'fs', + settings: { + location: '/tmp/', + }, }, - }, - verify: false, - }); + verify: false, + }) + .then(({ body }) => body); }; const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => { @@ -56,20 +58,27 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService']) policiesCreated.push(policy.name); } - return es.sr.updatePolicy({ - name: policy.name, - body: policy, - }); + return es.slm + .putLifecycle({ + policy_id: policy.name, + // TODO: bring {@link SlmPolicy} in line with {@link PutSnapshotLifecycleRequest['body']} + // @ts-expect-error + body: policy, + }) + .then(({ body }) => body); }; const getPolicy = (policyName: string) => { - return es.sr.policy({ - name: policyName, - human: true, - }); + return es.slm + .getLifecycle({ + policy_id: policyName, + human: true, + }) + .then(({ body }) => body); }; - const deletePolicy = (policyName: string) => es.sr.deletePolicy({ name: policyName }); + const deletePolicy = (policyName: string) => + es.slm.deleteLifecycle({ policy_id: policyName }).then(({ body }) => body); const cleanupPolicies = () => Promise.all(policiesCreated.map(deletePolicy)) diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 49482292bfb25..0b02d394b107f 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -10,7 +10,6 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; -import { elasticsearchJsPlugin as snapshotRestoreEsClientPlugin } from '../../../plugins/snapshot_restore/server/client/elasticsearch_sr'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -21,6 +20,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [indexManagementEsClientPlugin, snapshotRestoreEsClientPlugin], + plugins: [indexManagementEsClientPlugin], }); } diff --git a/x-pack/test/banners_functional/config.ts b/x-pack/test/banners_functional/config.ts new file mode 100644 index 0000000000000..21cce31ca5d85 --- /dev/null +++ b/x-pack/test/banners_functional/config.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import path from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services, pageObjects } from './ftr_provider_context'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const kibanaFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + testFiles: [require.resolve('./tests')], + servers: { + ...kibanaFunctionalConfig.get('servers'), + }, + services, + pageObjects, + + junit: { + reportName: 'X-Pack Banners Functional Tests', + }, + + esTestCluster: kibanaFunctionalConfig.get('esTestCluster'), + apps: { + ...kibanaFunctionalConfig.get('apps'), + }, + + esArchiver: { + directory: path.resolve(__dirname, '..', 'functional', 'es_archives'), + }, + + kbnTestServer: { + ...kibanaFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.banners.placement=header', + '--xpack.banners.textContent="global banner text"', + ], + }, + }; +} diff --git a/x-pack/test/banners_functional/ftr_provider_context.ts b/x-pack/test/banners_functional/ftr_provider_context.ts new file mode 100644 index 0000000000000..faac2954b00f6 --- /dev/null +++ b/x-pack/test/banners_functional/ftr_provider_context.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { services } from '../functional/services'; +import { pageObjects } from '../functional/page_objects'; + +export type FtrProviderContext = GenericFtrProviderContext; +export { services, pageObjects }; diff --git a/x-pack/test/banners_functional/tests/global.ts b/x-pack/test/banners_functional/tests/global.ts new file mode 100644 index 0000000000000..cef404d7ed132 --- /dev/null +++ b/x-pack/test/banners_functional/tests/global.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'security', 'banners']); + + describe('global pages', () => { + it('displays the global banner on the login page', async () => { + await PageObjects.common.navigateToApp('login'); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('global banner text'); + }); + }); +} diff --git a/x-pack/test/banners_functional/tests/index.ts b/x-pack/test/banners_functional/tests/index.ts new file mode 100644 index 0000000000000..301c872c746e1 --- /dev/null +++ b/x-pack/test/banners_functional/tests/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('banners - functional tests', function () { + this.tags('ciGroup2'); + + loadTestFile(require.resolve('./global')); + loadTestFile(require.resolve('./spaces')); + }); +} diff --git a/x-pack/test/banners_functional/tests/spaces.ts b/x-pack/test/banners_functional/tests/spaces.ts new file mode 100644 index 0000000000000..f8c412c0df0e3 --- /dev/null +++ b/x-pack/test/banners_functional/tests/spaces.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects([ + 'common', + 'security', + 'banners', + 'settings', + 'spaceSelector', + ]); + + describe('per-spaces banners', () => { + before(async () => { + await esArchiver.load('banners/multispace'); + }); + + after(async () => { + await esArchiver.unload('banners/multispace'); + }); + + before(async () => { + await PageObjects.security.login(undefined, undefined, { + expectSpaceSelector: true, + }); + await PageObjects.spaceSelector.clickSpaceCard('default'); + + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSettings(); + + await PageObjects.settings.setAdvancedSettingsTextArea( + 'banners:textContent', + 'default space banner text' + ); + }); + + it('displays the space-specific banner within the space', async () => { + await PageObjects.common.navigateToApp('home'); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('default space banner text'); + }); + + it('displays the global banner within another space', async () => { + await PageObjects.common.navigateToApp('home', { basePath: '/s/another-space' }); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('global banner text'); + }); + + it('displays the global banner on the login page', async () => { + await PageObjects.security.forceLogout(); + await PageObjects.common.navigateToApp('login'); + + expect(await PageObjects.banners.isTopBannerVisible()).to.eql(true); + expect(await PageObjects.banners.getTopBannerText()).to.eql('global banner text'); + }); + }); +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index a7925fa756693..f0b173d2d4c48 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -317,6 +317,16 @@ export default ({ getService }: FtrProviderContext) => { { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, first_seen: '2021-01-26T11:09:04.000Z', matched: { atomic: '159.89.119.67', @@ -339,6 +349,16 @@ export default ({ getService }: FtrProviderContext) => { { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, first_seen: '2021-01-26T11:09:04.000Z', matched: { atomic: '159.89.119.67', @@ -412,6 +432,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on ip', @@ -426,6 +456,16 @@ export default ({ getService }: FtrProviderContext) => { }, provider: 'other_provider', type: 'ip', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, ]); }); @@ -492,6 +532,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, // We do not merge matched indicators during enrichment, so in // certain circumstances a given indicator document could appear @@ -512,6 +562,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on ip', @@ -526,6 +586,16 @@ export default ({ getService }: FtrProviderContext) => { }, provider: 'other_provider', type: 'ip', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, ]); }); @@ -600,6 +670,16 @@ export default ({ getService }: FtrProviderContext) => { full: 'http://159.89.119.67:59600/bin.sh', scheme: 'http', }, + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, }, ]); @@ -621,6 +701,16 @@ export default ({ getService }: FtrProviderContext) => { full: 'http://159.89.119.67:59600/bin.sh', scheme: 'http', }, + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.595350Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978783/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on both port and ip', @@ -636,6 +726,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, { description: 'this should match auditbeat/hosts on both port and ip', @@ -651,6 +751,16 @@ export default ({ getService }: FtrProviderContext) => { port: 57324, provider: 'geenensp', type: 'url', + event: { + category: 'threat', + created: '2021-01-26T11:09:05.529Z', + dataset: 'threatintel.abuseurl', + ingested: '2021-01-26T11:09:06.616763Z', + kind: 'enrichment', + module: 'threatintel', + reference: 'https://urlhaus.abuse.ch/url/978782/', + type: 'indicator', + }, }, ]); }); diff --git a/x-pack/test/fleet_api_integration/apis/agents/status.ts b/x-pack/test/fleet_api_integration/apis/agents/status.ts index 3245b9a459fb1..f79ff15b64d33 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/status.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/status.ts @@ -79,6 +79,7 @@ export default function ({ getService }: FtrProviderContext) { offline: 1, updating: 1, other: 1, + inactive: 0, }, }); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts index 38d139c59430e..3b32ea031f6e2 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts @@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('feature controls', function () { this.tags(['skipFirefox']); loadTestFile(require.resolve('./dashboard_security')); + loadTestFile(require.resolve('./time_to_visualize_security')); loadTestFile(require.resolve('./dashboard_spaces')); }); } diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts new file mode 100644 index 0000000000000..3ebc53cc7cf27 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'timeToVisualize', + 'timePicker', + 'dashboard', + 'visEditor', + 'visualize', + 'security', + 'common', + 'header', + 'lens', + ]); + + const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardExpect = getService('dashboardExpect'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const find = getService('find'); + + describe('dashboard time to visualize security', () => { + before(async () => { + await esArchiver.load('dashboard/feature_controls/security'); + await esArchiver.loadIfNeeded('logstash_functional'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + + await security.role.create('dashboard_write_vis_read', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('dashboard_write_vis_read_user', { + password: 'dashboard_write_vis_read_user-password', + roles: ['dashboard_write_vis_read'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'dashboard_write_vis_read_user', + 'dashboard_write_vis_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('dashboard_write_vis_read'); + await security.user.delete('dashboard_write_vis_read_user'); + + await esArchiver.unload('dashboard/feature_controls/security'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('lens by value works without library save permissions', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('can add a lens panel by value', async () => { + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.lens.createAndAddLensFromDashboard({}); + const newPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.eql(1); + }); + + it('edits to a by value lens panel are properly applied', async () => { + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.lens.switchToVisualization('donut'); + await PageObjects.lens.saveAndReturn(); + await PageObjects.dashboard.waitForRenderComplete(); + + const pieExists = await find.existsByCssSelector('.lnsPieExpression__container'); + expect(pieExists).to.be(true); + }); + + it('disables save to library button without visualize save permissions', async () => { + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + const saveButton = await testSubjects.find('lnsApp_saveButton'); + expect(await saveButton.getAttribute('disabled')).to.equal('true'); + await PageObjects.lens.saveAndReturn(); + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new lens to be added by value, but not by reference', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + + await PageObjects.lens.configureDimension({ + dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', + operation: 'average', + field: 'bytes', + }); + + await PageObjects.lens.switchToVisualization('lnsMetric'); + + await PageObjects.lens.waitForVisualization(); + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('lnsApp_saveButton'); + + const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox'); + expect(await libraryCheckbox.getAttribute('disabled')).to.equal('true'); + + await PageObjects.timeToVisualize.saveFromModal('New Lens from Modal', { + addToDashboard: 'new', + saveAsNew: true, + saveToLibrary: false, + }); + + await PageObjects.dashboard.waitForRenderComplete(); + + await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); + const isLinked = await PageObjects.timeToVisualize.libraryNotificationExists( + 'New Lens from Modal' + ); + expect(isLinked).to.be(false); + + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + }); + + describe('visualize by value works without library save permissions', () => { + const originalMarkdownText = 'Original markdown text'; + const modifiedMarkdownText = 'Modified markdown text'; + + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('can add a markdown panel by value', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.waitForRenderComplete(); + + await testSubjects.click('dashboardAddNewPanelButton'); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); + await PageObjects.visEditor.clickGo(); + + await PageObjects.visualize.saveVisualizationAndReturn(); + const newPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.eql(1); + }); + + it('edits to a by value visualize panel are properly applied', async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visEditor.setMarkdownTxt(modifiedMarkdownText); + await PageObjects.visEditor.clickGo(); + await PageObjects.visualize.saveVisualizationAndReturn(); + + await PageObjects.dashboard.waitForRenderComplete(); + const markdownText = await testSubjects.find('markdownBody'); + expect(await markdownText.getVisibleText()).to.eql(modifiedMarkdownText); + + const newPanelCount = PageObjects.dashboard.getPanelCount(); + expect(newPanelCount).to.eql(1); + }); + + it('disables save to library button without visualize save permissions', async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.missingOrFail('visualizeSaveButton'); + await PageObjects.visualize.saveVisualizationAndReturn(); + await PageObjects.timeToVisualize.resetNewDashboard(); + }); + + it('should allow new visualization to be added by value, but not by reference', async function () { + await PageObjects.visualize.navigateToNewAggBasedVisualization(); + await PageObjects.visualize.clickMetric(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + + await testSubjects.click('visualizeSaveButton'); + + await PageObjects.visualize.ensureSavePanelOpen(); + const libraryCheckbox = await find.byCssSelector('#add-to-library-checkbox'); + expect(await libraryCheckbox.getAttribute('disabled')).to.equal('true'); + + await PageObjects.timeToVisualize.saveFromModal('My New Vis 1', { + addToDashboard: 'new', + }); + + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['14,005']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index d4a909f6a0474..c437cfaa8f5dc 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -50,7 +50,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel }; - describe('Download CSV', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 + describe.skip('Download CSV', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await browser.setWindowSize(1600, 850); diff --git a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap index 5ddef936b41ae..baa49cb6f9d81 100644 --- a/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap +++ b/x-pack/test/functional/apps/discover/__snapshots__/reporting.snap @@ -71,7 +71,7 @@ exports[`discover Discover CSV Export Generate CSV: new search generates a repor 24.5 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.375, 33, 10.344, 6.109\\",\\"80, 60, 21.984, 11.992\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",174,174,4,4,order,sultan " `; @@ -83,6 +83,6 @@ exports[`discover Discover CSV Export Generate CSV: new search generates a repor 24.5 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.375, 33, 10.344, 6.109\\",\\"80, 60, 21.984, 11.992\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",174,174,4,4,order,sultan " `; diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts index 9acb4c311c113..d7dd961e2f103 100644 --- a/x-pack/test/functional/apps/discover/reporting.ts +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -21,8 +21,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await kibanaServer.uiSettings.update({ 'discover:searchFieldsFromSource': setValue }); }; - // Failing: See https://github.com/elastic/kibana/issues/95592 - describe.skip('Discover CSV Export', () => { + describe('Discover CSV Export', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.load('reporting/ecommerce'); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 0cab12bc6672f..250a2d4ed71f9 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -12,7 +12,8 @@ export default function ({ getService, getPageObjects }) { const config = getService('config'); const log = getService('log'); - describe('users', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96001 + describe.skip('users', function () { before(async () => { log.debug('users'); await PageObjects.settings.navigateTo(); diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index 955618774bdfd..b72656a96980f 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'snapshotRestore']); const log = getService('log'); - const es = getService('legacyEs'); + const es = getService('es'); describe('Home page', function () { before(async () => { diff --git a/x-pack/test/functional/es_archives/banners/multispace/data.json b/x-pack/test/functional/es_archives/banners/multispace/data.json new file mode 100644 index 0000000000000..fc0e0dc7b7eee --- /dev/null +++ b/x-pack/test/functional/es_archives/banners/multispace/data.json @@ -0,0 +1,62 @@ +{ + "type": "doc", + "value": { + "id": "config:6.0.0", + "index": ".kibana", + "source": { + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "http://example.com/evil" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "id": "another-space:config:6.0.0", + "index": ".kibana", + "source": { + "namespace": "another-space", + "config": { + "buildNum": 8467, + "dateFormat:tz": "UTC", + "defaultRoute": "/app/canvas" + }, + "type": "config" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana", + "source": { + "space": { + "description": "This is the default space!", + "name": "Default" + }, + "type": "space" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:another-space", + "index": ".kibana", + "source": { + "space": { + "description": "This is another space", + "name": "Another Space" + }, + "type": "space" + } + } +} diff --git a/x-pack/test/functional/es_archives/banners/multispace/mappings.json b/x-pack/test/functional/es_archives/banners/multispace/mappings.json new file mode 100644 index 0000000000000..f3793c7ca6780 --- /dev/null +++ b/x-pack/test/functional/es_archives/banners/multispace/mappings.json @@ -0,0 +1,287 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultRoute": { + "type": "keyword" + } + } + }, + "dashboard": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "dynamic": "strict", + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "dynamic": "strict", + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "dynamic": "strict", + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "timelion-sheet": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "url": { + "dynamic": "strict", + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "dynamic": "strict", + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/banners_page.ts b/x-pack/test/functional/page_objects/banners_page.ts new file mode 100644 index 0000000000000..d2e4e43cec117 --- /dev/null +++ b/x-pack/test/functional/page_objects/banners_page.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function BannersPageProvider({ getService }: FtrProviderContext) { + const find = getService('find'); + + class BannersPage { + isTopBannerVisible() { + return find.existsByCssSelector('.header__topBanner .kbnUserBanner__container'); + } + + async getTopBannerText() { + if (!(await this.isTopBannerVisible())) { + return ''; + } + const bannerContainer = await find.byCssSelector( + '.header__topBanner .kbnUserBanner__container' + ); + return bannerContainer.getVisibleText(); + } + } + + return new BannersPage(); +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index cf92191075fba..81c0328e76342 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -41,6 +41,7 @@ import { TagManagementPageProvider } from './tag_management_page'; import { NavigationalSearchProvider } from './navigational_search'; import { SearchSessionsPageProvider } from './search_sessions_management_page'; import { DetectionsPageProvider } from '../../security_solution_ftr/page_objects/detections'; +import { BannersPageProvider } from './banners_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -78,5 +79,6 @@ export const pageObjects = { roleMappings: RoleMappingsPageProvider, ingestPipelines: IngestPipelinesPageProvider, navigationalSearch: NavigationalSearchProvider, + banners: BannersPageProvider, detections: DetectionsPageProvider, }; diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 205a4391062a2..65020be390f9d 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -197,7 +197,10 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }, async searchField(name: string) { - await testSubjects.setValue('lnsIndexPatternFieldSearch', name); + await testSubjects.setValue('lnsIndexPatternFieldSearch', name, { + clearWithKeyboard: true, + typeCharByChar: true, + }); }, async waitForField(field: string) { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 550e6ca455b22..7b760dfb8b6a1 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -54,7 +54,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('rulesTab'); } - // Failing: See https://github.com/elastic/kibana/issues/95590 + // FLAKY: https://github.com/elastic/kibana/issues/95591 describe.skip('alerts list', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); @@ -129,13 +129,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const disableSwitchAfterDisable = await testSubjects.find('disableSwitch'); - const isChecked = await disableSwitchAfterDisable.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'disableSwitch', + 'true' + ); }); it('should re-enable single alert', async () => { @@ -147,19 +145,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'disableSwitch', + 'true' + ); + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); await pageObjects.triggersActionsUI.toggleSwitch('disableSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const disableSwitchAfterReEnable = await testSubjects.find('disableSwitch'); - const isChecked = await disableSwitchAfterReEnable.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'disableSwitch', + 'false' + ); }); it('should mute single alert', async () => { @@ -171,13 +173,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const muteSwitchAfterMute = await testSubjects.find('muteSwitch'); - const isChecked = await muteSwitchAfterMute.getAttribute('aria-checked'); - expect(isChecked).to.eql('true'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'muteSwitch', + 'true' + ); }); it('should unmute single alert', async () => { @@ -189,19 +189,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'muteSwitch', + 'true' + ); + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); await testSubjects.click('collapsedItemActions'); await pageObjects.triggersActionsUI.toggleSwitch('muteSwitch'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - await testSubjects.click('collapsedItemActions'); - - const muteSwitchAfterUnmute = await testSubjects.find('muteSwitch'); - const isChecked = await muteSwitchAfterUnmute.getAttribute('aria-checked'); - expect(isChecked).to.eql('false'); + await pageObjects.triggersActionsUI.ensureRuleActionToggleApplied( + createdAlert.name, + 'muteSwitch', + 'false' + ); }); it('should delete single alert', async () => { diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index 8d4311a3ec322..e5971ddba415f 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -186,5 +186,18 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) expect(isConfirmationModalVisible).to.eql(true, 'Expect confirmation modal to be visible'); await testSubjects.click('confirmModalConfirmButton'); }, + async ensureRuleActionToggleApplied( + ruleName: string, + switchName: string, + shouldBeCheckedAsString: string + ) { + await retry.try(async () => { + await this.searchAlerts(ruleName); + await testSubjects.click('collapsedItemActions'); + const switchControl = await testSubjects.find(switchName); + const isChecked = await switchControl.getAttribute('aria-checked'); + expect(isChecked).to.eql(shouldBeCheckedAsString); + }); + }, }; } diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts index 5b846e414bd4c..104d11eb87f7c 100644 --- a/x-pack/test/plugin_functional/config.ts +++ b/x-pack/test/plugin_functional/config.ts @@ -30,6 +30,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './test_suites/resolver'), resolve(__dirname, './test_suites/global_search'), + resolve(__dirname, './test_suites/timelines'), ], services, @@ -47,6 +48,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { KIBANA_ROOT, 'test/plugin_functional/plugins/core_provider_plugin' )}`, + '--xpack.timelines.enabled=true', ...plugins.map((pluginDir) => `--plugin-path=${resolve(__dirname, 'plugins', pluginDir)}`), ], }, @@ -60,6 +62,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { resolverTest: { pathname: '/app/resolverTest', }, + timelineTest: { + pathname: '/app/timelinesTest', + }, }, // choose where esArchiver should load archives from diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json new file mode 100644 index 0000000000000..85c2639ef7d47 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/kibana.json @@ -0,0 +1,12 @@ +{ + "id": "timelinesTest", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "timelinesTest"], + "requiredPlugins": ["timelines"], + "requiredBundles": [ + "kibanaReact" + ], + "server": false, + "ui": true +} diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx new file mode 100644 index 0000000000000..a6772c3b0bb5b --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Router } from 'react-router-dom'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TimelinesPluginSetup } from '../../../../../../../plugins/timelines/public'; + +/** + * Render the Timeline Test app. Returns a cleanup function. + */ +export function renderApp( + coreStart: CoreStart, + parameters: AppMountParameters, + timelinesPluginSetup: TimelinesPluginSetup +) { + ReactDOM.render( + , + parameters.element + ); + + return () => { + ReactDOM.unmountComponentAtNode(parameters.element); + }; +} + +const AppRoot = React.memo( + ({ + coreStart, + parameters, + timelinesPluginSetup, + }: { + coreStart: CoreStart; + parameters: AppMountParameters; + timelinesPluginSetup: TimelinesPluginSetup; + }) => { + return ( + + + + {(timelinesPluginSetup.getTimeline && + timelinesPluginSetup.getTimeline({ timelineId: 'test' })) ?? + null} + + + + ); + } +); diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/index.ts b/x-pack/test/plugin_functional/plugins/timelines_test/public/index.ts new file mode 100644 index 0000000000000..5f038b5b933e6 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginInitializer } from 'kibana/public'; +import { + TimelinesTestPlugin, + TimelinesTestPluginSetupDependencies, + TimelinesTestPluginStartDependencies, +} from './plugin'; + +export const plugin: PluginInitializer< + void, + void, + TimelinesTestPluginSetupDependencies, + TimelinesTestPluginStartDependencies +> = () => new TimelinesTestPlugin(); diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts new file mode 100644 index 0000000000000..5cf900e194d0c --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/plugin.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { TimelinesPluginSetup } from '../../../../../plugins/timelines/public'; +import { renderApp } from './applications/timelines_test'; + +export type TimelinesTestPluginSetup = void; +export type TimelinesTestPluginStart = void; +export interface TimelinesTestPluginSetupDependencies { + timelines: TimelinesPluginSetup; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TimelinesTestPluginStartDependencies {} + +export class TimelinesTestPlugin + implements + Plugin< + TimelinesTestPluginSetup, + void, + TimelinesTestPluginSetupDependencies, + TimelinesTestPluginStartDependencies + > { + public setup( + core: CoreSetup, + setupDependencies: TimelinesTestPluginSetupDependencies + ) { + core.application.register({ + id: 'timelinesTest', + title: i18n.translate('xpack.timelinesTest.pluginTitle', { + defaultMessage: 'Timelines Test', + }), + mount: async (params: AppMountParameters) => { + const startServices = await core.getStartServices(); + const [coreStart] = startServices; + const { timelines } = setupDependencies; + + return renderApp(coreStart, params, timelines); + }, + }); + } + + public start() {} +} diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts index 077044d29f7d9..a44ded43a0bfe 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - describe('TOTO GlobalSearchBar', function () { + describe('GlobalSearchBar', function () { const { common, navigationalSearch } = getPageObjects(['common', 'navigationalSearch']); const esArchiver = getService('esArchiver'); const browser = getService('browser'); diff --git a/x-pack/test/plugin_functional/test_suites/timelines/index.ts b/x-pack/test/plugin_functional/test_suites/timelines/index.ts new file mode 100644 index 0000000000000..655ed9dc3898a --- /dev/null +++ b/x-pack/test/plugin_functional/test_suites/timelines/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + describe('Timelines plugin API', function () { + this.tags('ciGroup7'); + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + describe('timelines plugin rendering', function () { + before(async () => { + await pageObjects.common.navigateToApp('timelineTest'); + }); + it('shows the timeline component on navigation', async () => { + await testSubjects.existOrFail('timeline-wrapper'); + }); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap index c7ef39f65f552..094d72942353d 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap +++ b/x-pack/test/reporting_api_integration/reporting_and_security/__snapshots__/csv_searchsource_immediate.snap @@ -8,28 +8,28 @@ exports[`Reporting APIs CSV Generation from SearchSource Exports CSV with all fi 24.5 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.39, 32.99, 10.34, 6.11\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"79.99, 59.99, 21.99, 11.99\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"173.96\\",\\"173.96\\",4,4,order,sultan +}\\",\\"Abu Dhabi\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Angeldale, Oceanavigations, Microlutions\\",\\"Jul 12, 2019 @ 00:00:00.000\\",716724,\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"sold_product_716724_23975, sold_product_716724_6338, sold_product_716724_14116, sold_product_716724_15290\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Men's Shoes, Men's Clothing, Women's Accessories, Men's Accessories\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0, 0, 0\\",\\"0, 0, 0, 0\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"Angeldale, Oceanavigations, Microlutions, Oceanavigations\\",\\"42.375, 33, 10.344, 6.109\\",\\"80, 60, 21.984, 11.992\\",\\"23,975, 6,338, 14,116, 15,290\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"Winter boots - cognac, Trenchcoat - black, Watch - black, Hat - light grey multicolor\\",\\"1, 1, 1, 1\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",\\"0, 0, 0, 0\\",\\"80, 60, 21.984, 11.992\\",\\"80, 60, 21.984, 11.992\\",\\"0, 0, 0, 0\\",\\"ZO0687606876, ZO0290502905, ZO0126701267, ZO0308503085\\",174,174,4,4,order,sultan 9gMtOW0BH63Xcmy432DJ,ecommerce,\\"-\\",\\"-\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",EUR,Pia,Pia,\\"Pia Richards\\",\\"Pia Richards\\",FEMALE,45,Richards,Richards,,Saturday,5,\\"pia@richards-family.zzz\\",Cannes,Europe,FR,\\"{ \\"\\"coordinates\\"\\": [ 7, 43.6 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Alpes-Maritimes\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591503,\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"20.99, 20.99\\",\\"20.99, 20.99\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"10.7, 9.87\\",\\"20.99, 20.99\\",\\"14,761, 11,632\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"1, 1\\",\\"ZO0006400064, ZO0150601506\\",\\"0, 0\\",\\"20.99, 20.99\\",\\"20.99, 20.99\\",\\"0, 0\\",\\"ZO0006400064, ZO0150601506\\",\\"41.98\\",\\"41.98\\",2,2,order,pia +}\\",\\"Alpes-Maritimes\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591503,\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"sold_product_591503_14761, sold_product_591503_11632\\",\\"20.984, 20.984\\",\\"20.984, 20.984\\",\\"Women's Shoes, Women's Clothing\\",\\"Women's Shoes, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Tigress Enterprises, Pyramidustries\\",\\"Tigress Enterprises, Pyramidustries\\",\\"10.703, 9.867\\",\\"20.984, 20.984\\",\\"14,761, 11,632\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"Classic heels - blue, Summer dress - coral/pink\\",\\"1, 1\\",\\"ZO0006400064, ZO0150601506\\",\\"0, 0\\",\\"20.984, 20.984\\",\\"20.984, 20.984\\",\\"0, 0\\",\\"ZO0006400064, ZO0150601506\\",\\"41.969\\",\\"41.969\\",2,2,order,pia BgMtOW0BH63Xcmy432LJ,ecommerce,\\"-\\",\\"-\\",\\"Women's Clothing\\",\\"Women's Clothing\\",EUR,Brigitte,Brigitte,\\"Brigitte Meyer\\",\\"Brigitte Meyer\\",FEMALE,12,Meyer,Meyer,,Saturday,5,\\"brigitte@meyer-family.zzz\\",\\"New York\\",\\"North America\\",US,\\"{ \\"\\"coordinates\\"\\": [ -74, 40.8 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"New York\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591709,\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"7.99, 32.99\\",\\"7.99, 32.99\\",\\"Women's Clothing, Women's Clothing\\",\\"Women's Clothing, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"3.6, 17.48\\",\\"7.99, 32.99\\",\\"20,734, 7,539\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"1, 1\\",\\"ZO0638206382, ZO0038800388\\",\\"0, 0\\",\\"7.99, 32.99\\",\\"7.99, 32.99\\",\\"0, 0\\",\\"ZO0638206382, ZO0038800388\\",\\"40.98\\",\\"40.98\\",2,2,order,brigitte +}\\",\\"New York\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"Jul 12, 2019 @ 00:00:00.000\\",591709,\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"sold_product_591709_20734, sold_product_591709_7539\\",\\"7.988, 33\\",\\"7.988, 33\\",\\"Women's Clothing, Women's Clothing\\",\\"Women's Clothing, Women's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Spherecords, Tigress Enterprises\\",\\"Spherecords, Tigress Enterprises\\",\\"3.6, 17.484\\",\\"7.988, 33\\",\\"20,734, 7,539\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"Basic T-shirt - dark blue, Summer dress - scarab\\",\\"1, 1\\",\\"ZO0638206382, ZO0038800388\\",\\"0, 0\\",\\"7.988, 33\\",\\"7.988, 33\\",\\"0, 0\\",\\"ZO0638206382, ZO0038800388\\",\\"40.969\\",\\"40.969\\",2,2,order,brigitte KQMtOW0BH63Xcmy432LJ,ecommerce,\\"-\\",\\"-\\",\\"Men's Clothing\\",\\"Men's Clothing\\",EUR,Abd,Abd,\\"Abd Mccarthy\\",\\"Abd Mccarthy\\",MALE,52,Mccarthy,Mccarthy,,Saturday,5,\\"abd@mccarthy-family.zzz\\",Cairo,Africa,EG,\\"{ \\"\\"coordinates\\"\\": [ 31.3, 30.1 ], \\"\\"type\\"\\": \\"\\"Point\\"\\" -}\\",\\"Cairo Governorate\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"Jul 12, 2019 @ 00:00:00.000\\",590937,\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"28.99, 12.99\\",\\"28.99, 12.99\\",\\"Men's Clothing, Men's Clothing\\",\\"Men's Clothing, Men's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"13.34, 6.11\\",\\"28.99, 12.99\\",\\"14,438, 23,607\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"1, 1\\",\\"ZO0297602976, ZO0565605656\\",\\"0, 0\\",\\"28.99, 12.99\\",\\"28.99, 12.99\\",\\"0, 0\\",\\"ZO0297602976, ZO0565605656\\",\\"41.98\\",\\"41.98\\",2,2,order,abd +}\\",\\"Cairo Governorate\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"Jul 12, 2019 @ 00:00:00.000\\",590937,\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"sold_product_590937_14438, sold_product_590937_23607\\",\\"28.984, 12.992\\",\\"28.984, 12.992\\",\\"Men's Clothing, Men's Clothing\\",\\"Men's Clothing, Men's Clothing\\",\\"Dec 31, 2016 @ 00:00:00.000, Dec 31, 2016 @ 00:00:00.000\\",\\"0, 0\\",\\"0, 0\\",\\"Oceanavigations, Elitelligence\\",\\"Oceanavigations, Elitelligence\\",\\"13.344, 6.109\\",\\"28.984, 12.992\\",\\"14,438, 23,607\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"Jumper - dark grey multicolor, Print T-shirt - black\\",\\"1, 1\\",\\"ZO0297602976, ZO0565605656\\",\\"0, 0\\",\\"28.984, 12.992\\",\\"28.984, 12.992\\",\\"0, 0\\",\\"ZO0297602976, ZO0565605656\\",\\"41.969\\",\\"41.969\\",2,2,order,abd " `; diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts index ffaa4cb2f8fb6..ebc7badd88f42 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/csv_searchsource_immediate.ts @@ -31,7 +31,7 @@ export default function ({ getService }: FtrProviderContext) { }, }; - // Failing: See https://github.com/elastic/kibana/issues/95594 + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/96000 describe.skip('CSV Generation from SearchSource', () => { before(async () => { await kibanaServer.uiSettings.update({ diff --git a/yarn.lock b/yarn.lock index 486752dce5587..80ad1acf7fccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1349,10 +1349,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@26.0.0": - version "26.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-26.0.0.tgz#42f06d3be0f40e0128e301b37bdfc509169c387b" - integrity sha512-5eBPSjdBb+pVDCcQOYA0dFBiYonHcw7ewxOUxgR8qMmay0xHc7gGUXZiDfIkyUDpJA+a9DS9juNNqKn/M4UbiQ== +"@elastic/charts@26.1.0": + version "26.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-26.1.0.tgz#3c8677d84e52ac7209aee19484fb2b7e2a22e5cc" + integrity sha512-RiidG+9QIn17o5AW8cntrznH+MaOO8gIAwrkJW1EMInntZgEA66WhVs4Kg2Negp6hsPMMeArQVWbDhXE9ST3qg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0"