From 50d21dad8c89c711459da4eb774c190e1ee6cb5a Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 31 Mar 2021 15:59:41 +0200 Subject: [PATCH 1/5] [Lens] Fix runtime field test flakiness (#95650) --- x-pack/test/functional/page_objects/lens_page.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) { From 5b3d88749ea87b09b1357cbb37349eca467c1e29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:15:50 +0200 Subject: [PATCH 2/5] Update dependency @elastic/charts to v26.1.0 (#95582) Co-authored-by: Renovate Bot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) 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/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" From 53584c694d1acf3221342f9346c5b8bd5914ae9c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 31 Mar 2021 16:19:31 +0200 Subject: [PATCH 3/5] [Lens] Introduce single percentile and make it filterable in Lens (#95439) --- ...aggfunctionsmapping.aggsinglepercentile.md | 11 ++ ...plugins-data-public.aggfunctionsmapping.md | 1 + ...plugin-plugins-data-public.metric_types.md | 1 + ...aggfunctionsmapping.aggsinglepercentile.md | 11 ++ ...plugins-data-server.aggfunctionsmapping.md | 1 + ...plugin-plugins-data-server.metric_types.md | 1 + .../data/common/search/aggs/agg_types.ts | 2 + .../common/search/aggs/aggs_service.test.ts | 2 + .../single_percentile.test.ts.snap | 17 ++ .../data/common/search/aggs/metrics/index.ts | 2 + .../metrics/lib/parent_pipeline_agg_helper.ts | 1 + .../lib/sibling_pipeline_agg_helper.ts | 1 + .../search/aggs/metrics/metric_agg_types.ts | 1 + .../aggs/metrics/single_percentile.test.ts | 145 ++++++++++++++++++ .../search/aggs/metrics/single_percentile.ts | 63 ++++++++ .../aggs/metrics/single_percentile_fn.ts | 94 ++++++++++++ src/plugins/data/common/search/aggs/types.ts | 4 + src/plugins/data/public/public.api.md | 12 +- .../public/search/aggs/aggs_service.test.ts | 4 +- src/plugins/data/server/server.api.md | 8 +- .../vis_type_metric/public/metric_vis_type.ts | 1 + .../public/legacy/table_vis_legacy_type.ts | 2 +- .../vis_type_table/public/table_vis_type.ts | 2 +- .../public/tag_cloud_type.ts | 1 + src/plugins/vis_type_vislib/public/gauge.ts | 1 + src/plugins/vis_type_vislib/public/goal.ts | 1 + src/plugins/vis_type_vislib/public/heatmap.ts | 1 + .../vis_type_xy/public/vis_types/area.ts | 2 +- .../vis_type_xy/public/vis_types/histogram.ts | 2 +- .../public/vis_types/horizontal_bar.ts | 2 +- .../vis_type_xy/public/vis_types/line.ts | 2 +- .../indexpattern.test.ts | 47 ------ .../operations/definitions/helpers.tsx | 7 - .../operations/definitions/index.ts | 7 - .../definitions/percentile.test.tsx | 2 +- .../operations/definitions/percentile.tsx | 23 +-- .../operations/definitions/terms/index.tsx | 8 +- .../definitions/terms/terms.test.tsx | 40 ----- .../indexpattern_datasource/to_expression.ts | 4 +- 39 files changed, 403 insertions(+), 134 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggfunctionsmapping.aggsinglepercentile.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.aggfunctionsmapping.aggsinglepercentile.md create mode 100644 src/plugins/data/common/search/aggs/metrics/__snapshots__/single_percentile.test.ts.snap create mode 100644 src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/single_percentile.ts create mode 100644 src/plugins/data/common/search/aggs/metrics/single_percentile_fn.ts 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/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.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/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 e14c908023ac6..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", 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 2b5bb6596cef9..675be2323b93e 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -56,6 +56,7 @@ import { AggParamsIpRange, AggParamsMax, AggParamsMedian, + AggParamsSinglePercentile, AggParamsMin, AggParamsMovingAvg, AggParamsPercentileRanks, @@ -85,6 +86,7 @@ import { METRIC_TYPES, AggConfig, aggFilteredMetric, + aggSinglePercentile, } from './'; export { IAggConfig, AggConfigSerialized } from './agg_config'; @@ -169,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; @@ -215,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/public/public.api.md b/src/plugins/data/public/public.api.md index be57435aa29d4..7f243cefd08b6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -399,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) @@ -717,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 @@ -726,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; +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) // @@ -1860,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", @@ -2650,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:127: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 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 920a6a81cb5fd..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); }); }); diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 6d3eac01dc4dc..1f043cc9aab4e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -202,6 +202,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 +407,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) // @@ -1163,6 +1167,8 @@ export enum METRIC_TYPES { // (undocumented) SERIAL_DIFF = "serial_diff", // (undocumented) + SINGLE_PERCENTILE = "single_percentile", + // (undocumented) STD_DEV = "std_dev", // (undocumented) SUM = "sum", 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_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/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/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, }, From 73109fb3abe47cf39fde658184690c274b06fb95 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 31 Mar 2021 07:25:57 -0700 Subject: [PATCH 4/5] Revert "[Time to Visualize] Allow By Value Flow Without Visualize Save Permissions (#93846)" This reverts commit cfe5f9c016625e6d6233f8d3a868308fdbba3d3c. --- .../actions/add_to_library_action.test.tsx | 50 +--- .../actions/add_to_library_action.tsx | 18 +- .../actions/clone_panel_action.tsx | 3 +- src/plugins/dashboard/public/plugin.tsx | 11 +- src/plugins/dashboard/public/services/core.ts | 1 - .../saved_object_save_modal_dashboard.tsx | 10 +- ..._save_modal_dashboard_selector.stories.tsx | 7 - ...d_object_save_modal_dashboard_selector.tsx | 6 +- .../public/services/index.ts | 1 - .../public/services/kibana/capabilities.ts | 3 +- .../public/services/storybook/capabilities.ts | 2 - .../public/services/storybook/index.ts | 1 - .../public/services/stub/capabilities.ts | 1 - .../create_vis_embeddable_from_object.ts | 7 +- .../public/embeddable/visualize_embeddable.ts | 11 +- .../application/utils/get_top_nav_config.tsx | 26 +- .../public/application/utils/utils.ts | 2 +- x-pack/plugins/lens/public/app_plugin/app.tsx | 17 +- .../lens/public/app_plugin/lens_top_nav.tsx | 25 +- .../lens/public/app_plugin/save_modal.tsx | 6 +- .../embeddable/embeddable.test.tsx | 64 ++--- .../embeddable/embeddable.tsx | 13 +- .../embeddable/embeddable_factory.ts | 8 +- .../public/routes/map_page/top_nav_config.tsx | 6 +- .../apps/dashboard/feature_controls/index.ts | 1 - .../time_to_visualize_security.ts | 233 ------------------ 26 files changed, 65 insertions(+), 468 deletions(-) delete mode 100644 x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 1156bf8c6e0d1..07c38fd406eea 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -41,15 +41,8 @@ const start = doStart(); let container: DashboardContainer; let embeddable: ContactCardEmbeddable & ReferenceOrValueEmbeddable; let coreStart: CoreStart; -let capabilities: CoreStart['application']['capabilities']; - beforeEach(async () => { coreStart = coreMock.createStart(); - capabilities = { - ...coreStart.application.capabilities, - visualize: { save: true }, - maps: { save: true }, - }; const containerOptions = { ExitFullScreenButton: () => null, @@ -90,10 +83,7 @@ beforeEach(async () => { }); test('Add to library is incompatible with Error Embeddables', async () => { - const action = new AddToLibraryAction({ - toasts: coreStart.notifications.toasts, - capabilities, - }); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); const errorEmbeddable = new ErrorEmbeddable( 'Wow what an awful error', { id: ' 404' }, @@ -102,37 +92,20 @@ 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, - capabilities, - }); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); 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, - capabilities, - }); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); 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, - capabilities, - }); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); @@ -153,10 +126,7 @@ 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, - capabilities, - }); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); @@ -165,10 +135,7 @@ 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, - capabilities, - }); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); @@ -194,10 +161,7 @@ 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, - capabilities, - }); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); 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 fa102a9415b3f..ef730e16bc5cf 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 { ApplicationStart, NotificationsStart } from '../../services/core'; +import { NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; @@ -33,12 +33,7 @@ export class AddToLibraryAction implements Action { public readonly id = ACTION_ADD_TO_LIBRARY; public order = 15; - constructor( - private deps: { - toasts: NotificationsStart['toasts']; - capabilities: ApplicationStart['capabilities']; - } - ) {} + constructor(private deps: { toasts: NotificationsStart['toasts'] }) {} public getDisplayName({ embeddable }: AddToLibraryActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { @@ -55,15 +50,8 @@ 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( - canSave && - !isErrorEmbeddable(embeddable) && + !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 829344504b16b..c82f17f2b29c4 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -61,8 +61,7 @@ export class ClonePanelAction implements Action { embeddable.getInput()?.viewMode !== ViewMode.VIEW && embeddable.getRoot() && embeddable.getRoot().isContainer && - embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE && - embeddable.getOutput().editable + embeddable.getRoot().type === DASHBOARD_CONTAINER_TYPE ); } diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 5bf730996ab4f..ae2d2b5f237c9 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, application } = core; + const { notifications, overlays } = core; const { uiActions, data, share, presentationUtil, embeddable } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); @@ -370,10 +370,7 @@ export class DashboardPlugin } if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { - const addToLibraryAction = new AddToLibraryAction({ - toasts: notifications.toasts, - capabilities: application.capabilities, - }); + const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(addToLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); @@ -389,8 +386,8 @@ export class DashboardPlugin overlays, embeddable.getStateTransfer(), { - canCreateNew: Boolean(application.capabilities.dashboard.createNew), - canEditExisting: !Boolean(application.capabilities.dashboard.hideWriteControls), + canCreateNew: Boolean(core.application.capabilities.dashboard.createNew), + canEditExisting: !Boolean(core.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 75461729841e9..7c19b2d75a967 100644 --- a/src/plugins/dashboard/public/services/core.ts +++ b/src/plugins/dashboard/public/services/core.ts @@ -12,5 +12,4 @@ export { PluginInitializerContext, ScopedHistory, NotificationsStart, - ApplicationStart, } from '../../../../core/public'; 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 4491be04b1a42..57d05262319f2 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,7 +28,6 @@ interface SaveModalDocumentInfo { export interface SaveModalDashboardProps { documentInfo: SaveModalDocumentInfo; - canSaveByReference: boolean; objectType: string; onClose: () => void; onSave: (props: OnSaveProps & { dashboardId: string | null; addToLibrary: boolean }) => void; @@ -36,7 +35,7 @@ export interface SaveModalDashboardProps { } export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { - const { documentInfo, tagOptions, objectType, onClose, canSaveByReference } = props; + const { documentInfo, tagOptions, objectType, onClose } = props; const { id: documentId } = documentInfo; const initialCopyOnSave = !Boolean(documentId); @@ -50,7 +49,7 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { documentId || disableDashboardOptions ? null : 'existing' ); const [isAddToLibrarySelected, setAddToLibrary] = useState( - canSaveByReference && (!initialCopyOnSave || disableDashboardOptions) + !initialCopyOnSave || disableDashboardOptions ); const [selectedDashboard, setSelectedDashboard] = useState<{ id: string; name: string } | null>( null @@ -66,16 +65,13 @@ export function SavedObjectSaveModalDashboard(props: SaveModalDashboardProps) { onChange={(option) => { setDashboardOption(option); }} - canSaveByReference={canSaveByReference} {...{ copyOnSave, documentId, dashboardOption, setAddToLibrary, isAddToLibrarySelected }} /> ) : null; const onCopyOnSaveChange = (newCopyOnSave: boolean) => { - if (canSaveByReference) { - setAddToLibrary(true); - } + 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 341f194b71ba4..dd6fd975f8e07 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,21 +33,15 @@ 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); @@ -58,7 +52,6 @@ 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 78a1569c02ead..1ae54040571a2 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,7 +30,6 @@ export interface SaveModalDashboardSelectorProps { copyOnSave: boolean; documentId?: string; onSelectDashboard: DashboardPickerProps['onChange']; - canSaveByReference: boolean; setAddToLibrary: (selected: boolean) => void; isAddToLibrarySelected: boolean; dashboardOption: 'new' | 'existing' | null; @@ -41,7 +40,6 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp const { documentId, onSelectDashboard, - canSaveByReference, setAddToLibrary, isAddToLibrarySelected, dashboardOption, @@ -116,7 +114,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp setAddToLibrary(true); onChange(null); }} - disabled={isDisabled || !canSaveByReference} + disabled={isDisabled} /> @@ -129,7 +127,7 @@ export function SaveModalDashboardSelector(props: SaveModalDashboardSelectorProp defaultMessage: 'Add to library', })} checked={isAddToLibrarySelected} - disabled={dashboardOption === null || isDisabled || !canSaveByReference} + disabled={dashboardOption === null || isDisabled} 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 39dae92aa2ba9..74607b9e35e47 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -23,7 +23,6 @@ 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 6949fba00c65a..546281d083f2f 100644 --- a/src/plugins/presentation_util/public/services/kibana/capabilities.ts +++ b/src/plugins/presentation_util/public/services/kibana/capabilities.ts @@ -16,11 +16,10 @@ export type CapabilitiesServiceFactory = KibanaPluginServiceFactory< >; export const capabilitiesServiceFactory: CapabilitiesServiceFactory = ({ coreStart }) => { - const { dashboard, visualize } = coreStart.application.capabilities; + const { dashboard } = 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 16fbe3baf488f..fcd38b29f154c 100644 --- a/src/plugins/presentation_util/public/services/storybook/capabilities.ts +++ b/src/plugins/presentation_util/public/services/storybook/capabilities.ts @@ -19,13 +19,11 @@ 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 dd7de54264062..37b2171635e96 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -18,7 +18,6 @@ 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 4154fa65a0cd7..979ccc8faadd5 100644 --- a/src/plugins/presentation_util/public/services/stub/capabilities.ts +++ b/src/plugins/presentation_util/public/services/stub/capabilities.ts @@ -15,5 +15,4 @@ export const capabilitiesServiceFactory: CapabilitiesServiceFactory = () => ({ canAccessDashboards: () => true, canCreateNewDashboards: () => true, canEditDashboards: () => true, - canSaveVisualizations: () => true, }); 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 e2e2a4c089270..989a9bf5d2cb7 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,10 +67,7 @@ export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDe indexPatterns = [vis.data.indexPattern]; } - const capabilities = { - visualizeSave: Boolean(getCapabilities().visualize.save), - dashboardSave: Boolean(getCapabilities().dashboard?.showWriteControls), - }; + const editable = getCapabilities().visualize.save as boolean; return new VisualizeEmbeddable( getTimeFilter(), @@ -79,8 +76,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 429dabeeef042..580ffef548fe1 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; - capabilities: { visualizeSave: boolean; dashboardSave: boolean }; + editable: boolean; deps: VisualizeEmbeddableFactoryDeps; } @@ -111,7 +111,7 @@ export class VisualizeEmbeddable constructor( timefilter: TimefilterContract, - { vis, editPath, editUrl, indexPatterns, deps, capabilities }: VisualizeEmbeddableConfiguration, + { vis, editPath, editUrl, indexPatterns, editable, deps }: VisualizeEmbeddableConfiguration, initialInput: VisualizeInput, attributeService?: AttributeService< VisualizeSavedObjectAttributes, @@ -129,6 +129,7 @@ export class VisualizeEmbeddable editApp: 'visualize', editUrl, indexPatterns, + editable, visTypeName: vis.type.name, }, parent @@ -142,12 +143,6 @@ 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 e696bcb5dbe4d..4f5679a14b0b7 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,7 +82,6 @@ export const getTopNavConfig = ( setActiveUrl, toastNotifications, visualizeCapabilities, - dashboardCapabilities, i18n: { Context: I18nContext }, dashboard, savedObjectsTagging, @@ -206,9 +205,9 @@ export const getTopNavConfig = ( } }; - const allowByValue = dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; const saveButtonLabel = - embeddableId || (!savedVis.id && allowByValue && originatingApp) + embeddableId || + (!savedVis.id && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && originatingApp) ? i18n.translate('visualize.topNavMenu.saveVisualizationToLibraryButtonLabel', { defaultMessage: 'Save to library', }) @@ -220,11 +219,9 @@ export const getTopNavConfig = ( defaultMessage: 'Save', }); - const showSaveAndReturn = originatingApp && (savedVis?.id || allowByValue); - - const showSaveButton = - visualizeCapabilities.save || - (allowByValue && !showSaveAndReturn && dashboardCapabilities.showWriteControls); + const showSaveAndReturn = + originatingApp && + (savedVis?.id || dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables); const topNavMenu: TopNavMenuData[] = [ { @@ -303,7 +300,7 @@ export const getTopNavConfig = ( }, ] : []), - ...(showSaveButton + ...(visualizeCapabilities.save ? [ { id: 'save', @@ -442,12 +439,7 @@ export const getTopNavConfig = ( /> ) : ( { defaultMessage: 'Read only', }), tooltip: i18n.translate('visualize.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save visualizations to the library', + defaultMessage: 'Unable to save visualizations', }), iconType: 'glasses', }); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index dbc10c751a649..9d5feec9f21e6 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -531,13 +531,7 @@ export function App({ const { TopNavMenu } = navigation.ui; - const savingToLibraryPermitted = Boolean( - state.isSaveable && application.capabilities.visualize.save - ); - const savingToDashboardPermitted = Boolean( - state.isSaveable && application.capabilities.dashboard?.showWriteControls - ); - + const savingPermitted = Boolean(state.isSaveable && application.capabilities.visualize.save); const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { defaultMessage: 'unsaved', }); @@ -551,10 +545,8 @@ export function App({ state.isSaveable && state.activeData && Object.keys(state.activeData).length ), isByValueMode: getIsByValueMode(), - allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, showCancel: Boolean(state.isLinkedToOriginatingApp), - savingToLibraryPermitted, - savingToDashboardPermitted, + savingPermitted, actions: { exportToCSV: () => { if (!state.activeData) { @@ -585,7 +577,7 @@ export function App({ } }, saveAndReturn: () => { - if (savingToDashboardPermitted && lastKnownDoc) { + if (savingPermitted && lastKnownDoc) { // disabling the validation on app leave because the document has been saved. onAppLeave((actions) => { return actions.default(); @@ -605,7 +597,7 @@ export function App({ } }, showSaveModal: () => { - if (savingToDashboardPermitted || savingToLibraryPermitted) { + if (savingPermitted) { setState((s) => ({ ...s, isSaveModalVisible: true })); } }, @@ -705,7 +697,6 @@ export function App({ { const { originatingApp, - savingToLibraryPermitted, savedObjectsTagging, tagsIds, lastKnownDoc, @@ -87,15 +85,13 @@ export const SaveModal = (props: Props) => { { const saveToLibrary = Boolean(saveProps.addToLibrary); onSave(saveProps, { saveToLibrary }); }} onClose={onClose} documentInfo={{ - // if the user cannot save to the library - treat this as a new document. - id: savingToLibraryPermitted ? lastKnownDoc.savedObjectId : undefined, + id: lastKnownDoc.savedObjectId, 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 a44870b427ab1..157975b630e1e 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,10 +112,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -154,7 +151,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -194,10 +191,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -237,10 +231,7 @@ describe('embeddable', () => { indexPatternService: ({ get: (id: string) => Promise.resolve({ id }), } as unknown) as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -275,10 +266,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -319,7 +307,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -364,10 +352,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -410,10 +395,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -463,10 +445,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -516,10 +495,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -568,10 +544,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: ({ get: jest.fn() } as unknown) as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -609,10 +582,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -650,10 +620,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: true, getTrigger, documentToExpression: () => Promise.resolve({ @@ -691,10 +658,7 @@ describe('embeddable', () => { expressionRenderer, basePath, indexPatternService: {} as IndexPatternsContract, - capabilities: { - canSaveDashboards: true, - canSaveVisualizations: true, - }, + editable: 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 a3316e0083d35..1db067606dc82 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,6 +129,7 @@ export class Embeddable initialInput, { editApp: 'lens', + editable: deps.editable, }, parent ); @@ -325,7 +326,7 @@ export class Embeddable hasCompatibleActions={this.hasCompatibleActions} className={input.className} style={input.style} - canEdit={this.getIsEditable() && input.viewMode === 'edit'} + canEdit={this.deps.editable && input.viewMode === 'edit'} />, domNode ); @@ -450,7 +451,6 @@ 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,13 +458,6 @@ 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 1a4962bd1fe8e..a676b7283671c 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 Boolean(capabilities.visualize.save || capabilities.dashboard?.showWriteControls); + return capabilities.visualize.save as boolean; }; canCreateNew() { @@ -86,7 +86,6 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { coreHttp, attributeService, indexPatternService, - capabilities, } = await this.getStartServices(); const { Embeddable } = await import('../../async_services'); @@ -97,14 +96,11 @@ 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/maps/public/routes/map_page/top_nav_config.tsx b/x-pack/plugins/maps/public/routes/map_page/top_nav_config.tsx index 7e0aa59756876..597cd8e9c4287 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,11 +201,7 @@ export function getTopNavConfig({ options={tagSelector} /> ) : ( - + ); showSaveModal(saveModal, getCoreI18n().Context, PresentationUtilContext); 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 3b32ea031f6e2..38d139c59430e 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts @@ -11,7 +11,6 @@ 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 deleted file mode 100644 index 3ebc53cc7cf27..0000000000000 --- a/x-pack/test/functional/apps/dashboard/feature_controls/time_to_visualize_security.ts +++ /dev/null @@ -1,233 +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 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); - }); - }); - }); -} From 5f487292fbd3e45f4764199785c2b5ce0fa21df0 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 31 Mar 2021 08:50:40 -0600 Subject: [PATCH 5/5] [Maps] convert ToolbarOverlay to TS (#95368) * [Maps] convert ToolbarOverlay to TS * remove getDerivedStateFromProps * remove unused function Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/maps/public/actions/ui_actions.ts | 12 -- .../data_mapping/data_mapping_popover.tsx | 6 +- .../add_tooltip_field_popover.tsx | 6 +- .../map_container/map_container.tsx | 1 - .../toolbar_overlay.test.tsx.snap | 7 +- .../toolbar_overlay/index.js | 16 --- .../toolbar_overlay/index.ts | 8 ++ .../set_view_control/{index.js => index.ts} | 20 ++-- ...t_view_control.js => set_view_control.tsx} | 103 ++++++++++-------- .../toolbar_overlay/toolbar_overlay.js | 53 --------- .../toolbar_overlay/toolbar_overlay.test.tsx | 17 ++- .../toolbar_overlay/toolbar_overlay.tsx | 61 +++++++++++ x-pack/plugins/maps/public/reducers/ui.ts | 8 -- .../maps/public/selectors/ui_selectors.ts | 1 - 14 files changed, 162 insertions(+), 157 deletions(-) delete mode 100644 x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js create mode 100644 x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts rename x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/{index.js => index.ts} (62%) rename x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/{set_view_control.js => set_view_control.tsx} (69%) delete mode 100644 x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js create mode 100644 x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx 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/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/maps/public/connected_components/toolbar_overlay/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts new file mode 100644 index 0000000000000..d1008edfd572d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { 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/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/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;