[Lens] Implement filtered metric (#92589) (#95282)

This commit is contained in:
Joe Reuter 2021-03-24 13:57:44 +01:00 committed by GitHub
parent 296f81cf05
commit def8204814
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 6464 additions and 1468 deletions

View file

@ -1597,7 +1597,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 3160
"lineNumber": 174
},
"signature": [
{
@ -1816,7 +1816,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 558
"lineNumber": 56
},
"signature": [
{
@ -1837,7 +1837,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 559
"lineNumber": 57
}
},
{
@ -1848,7 +1848,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 562
"lineNumber": 60
},
"signature": [
"[number, number[]][]"
@ -1859,7 +1859,7 @@
"label": "[ColorSchemas.Greens]",
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 557
"lineNumber": 55
}
},
{
@ -1875,7 +1875,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1078
"lineNumber": 74
},
"signature": [
{
@ -1896,7 +1896,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1079
"lineNumber": 75
}
},
{
@ -1907,7 +1907,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1082
"lineNumber": 78
},
"signature": [
"[number, number[]][]"
@ -1918,7 +1918,7 @@
"label": "[ColorSchemas.Greys]",
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1077
"lineNumber": 73
}
},
{
@ -1934,7 +1934,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1598
"lineNumber": 92
},
"signature": [
{
@ -1955,7 +1955,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1599
"lineNumber": 93
}
},
{
@ -1966,7 +1966,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1602
"lineNumber": 96
},
"signature": [
"[number, number[]][]"
@ -1977,7 +1977,7 @@
"label": "[ColorSchemas.Reds]",
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 1597
"lineNumber": 91
}
},
{
@ -1993,7 +1993,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2118
"lineNumber": 110
},
"signature": [
{
@ -2014,7 +2014,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2119
"lineNumber": 111
}
},
{
@ -2025,7 +2025,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2122
"lineNumber": 114
},
"signature": [
"[number, number[]][]"
@ -2036,7 +2036,7 @@
"label": "[ColorSchemas.YellowToRed]",
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2117
"lineNumber": 109
}
},
{
@ -2052,7 +2052,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2639
"lineNumber": 129
},
"signature": [
{
@ -2073,7 +2073,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2640
"lineNumber": 130
}
},
{
@ -2084,7 +2084,7 @@
"description": [],
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2643
"lineNumber": 133
},
"signature": [
"[number, number[]][]"
@ -2095,7 +2095,7 @@
"label": "[ColorSchemas.GreenToRed]",
"source": {
"path": "src/plugins/charts/public/static/color_maps/color_maps.ts",
"lineNumber": 2638
"lineNumber": 128
}
}
],

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -21952,6 +21952,41 @@
"returnComment": [],
"initialIsOpen": false
},
{
"id": "def-common.createMockContext",
"type": "Function",
"children": [],
"signature": [
"() => ",
{
"pluginId": "expressions",
"scope": "common",
"docId": "kibExpressionsPluginApi",
"section": "def-common.ExecutionContext",
"text": "ExecutionContext"
},
"<",
{
"pluginId": "inspector",
"scope": "common",
"docId": "kibInspectorPluginApi",
"section": "def-common.Adapters",
"text": "Adapters"
},
", ",
"SerializableState",
">"
],
"description": [],
"label": "createMockContext",
"source": {
"path": "src/plugins/expressions/common/util/test_utils.ts",
"lineNumber": 11
},
"tags": [],
"returnComment": [],
"initialIsOpen": false
},
{
"id": "def-common.format",
"type": "Function",
@ -22465,6 +22500,52 @@
"tags": [],
"returnComment": [],
"initialIsOpen": false
},
{
"id": "def-common.unboxExpressionValue",
"type": "Function",
"label": "unboxExpressionValue",
"signature": [
"({\n type,\n ...value\n}: ",
{
"pluginId": "expressions",
"scope": "common",
"docId": "kibExpressionsPluginApi",
"section": "def-common.ExpressionValueBoxed",
"text": "ExpressionValueBoxed"
},
"<string, T>) => T"
],
"description": [],
"children": [
{
"type": "CompoundType",
"label": "{\n type,\n ...value\n}",
"isRequired": true,
"signature": [
{
"pluginId": "expressions",
"scope": "common",
"docId": "kibExpressionsPluginApi",
"section": "def-common.ExpressionValueBoxed",
"text": "ExpressionValueBoxed"
},
"<string, T>"
],
"description": [],
"source": {
"path": "src/plugins/expressions/common/expression_types/unbox_expression_value.ts",
"lineNumber": 11
}
}
],
"tags": [],
"returnComment": [],
"source": {
"path": "src/plugins/expressions/common/expression_types/unbox_expression_value.ts",
"lineNumber": 11
},
"initialIsOpen": false
}
],
"interfaces": [

File diff suppressed because it is too large Load diff

View file

@ -288,7 +288,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts",
"lineNumber": 44
"lineNumber": 46
},
"signature": [
"\"range\" | \"filters\" | \"count\" | \"max\" | \"min\" | \"date_histogram\" | \"sum\" | \"terms\" | \"avg\" | \"median\" | \"cumulative_sum\" | \"derivative\" | \"moving_average\" | \"counter_rate\" | \"cardinality\" | \"percentile\" | \"last_value\" | undefined"
@ -302,7 +302,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts",
"lineNumber": 45
"lineNumber": 47
},
"signature": [
"string | undefined"
@ -311,7 +311,7 @@
],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts",
"lineNumber": 43
"lineNumber": 45
},
"initialIsOpen": false
},
@ -1318,7 +1318,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx",
"lineNumber": 125
"lineNumber": 127
},
"signature": [
"BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"avg\"; }"
@ -1475,7 +1475,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx",
"lineNumber": 127
"lineNumber": 129
},
"signature": [
"BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"max\"; }"
@ -1490,7 +1490,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx",
"lineNumber": 128
"lineNumber": 130
},
"signature": [
"BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"median\"; }"
@ -1505,7 +1505,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx",
"lineNumber": 126
"lineNumber": 128
},
"signature": [
"BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"min\"; }"
@ -1538,7 +1538,7 @@
],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts",
"lineNumber": 405
"lineNumber": 406
},
"signature": [
"\"range\" | \"filters\" | \"count\" | \"max\" | \"min\" | \"date_histogram\" | \"sum\" | \"terms\" | \"avg\" | \"median\" | \"cumulative_sum\" | \"derivative\" | \"moving_average\" | \"counter_rate\" | \"cardinality\" | \"percentile\" | \"last_value\""
@ -1598,7 +1598,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx",
"lineNumber": 124
"lineNumber": 126
},
"signature": [
"BaseIndexPatternColumn & { params?: { format?: { id: string; params?: { decimals: number; } | undefined; } | undefined; } | undefined; } & FieldBasedIndexPatternColumn & { operationType: \"sum\"; }"

View file

@ -2312,7 +2312,7 @@
"description": [],
"source": {
"path": "x-pack/plugins/observability/server/plugin.ts",
"lineNumber": 22
"lineNumber": 23
},
"signature": [
"LazyScopedAnnotationsClientFactory"
@ -2321,7 +2321,7 @@
],
"source": {
"path": "x-pack/plugins/observability/server/plugin.ts",
"lineNumber": 21
"lineNumber": 22
},
"lifecycle": "setup",
"initialIsOpen": true

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [AggFunctionsMapping](./kibana-plugin-plugins-data-public.aggfunctionsmapping.md) &gt; [aggFilteredMetric](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md)
## AggFunctionsMapping.aggFilteredMetric property
<b>Signature:</b>
```typescript
aggFilteredMetric: ReturnType<typeof aggFilteredMetric>;
```

View file

@ -28,6 +28,7 @@ export interface AggFunctionsMapping
| [aggDateRange](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggdaterange.md) | <code>ReturnType&lt;typeof aggDateRange&gt;</code> | |
| [aggDerivative](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggderivative.md) | <code>ReturnType&lt;typeof aggDerivative&gt;</code> | |
| [aggFilter](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilter.md) | <code>ReturnType&lt;typeof aggFilter&gt;</code> | |
| [aggFilteredMetric](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilteredmetric.md) | <code>ReturnType&lt;typeof aggFilteredMetric&gt;</code> | |
| [aggFilters](./kibana-plugin-plugins-data-public.aggfunctionsmapping.aggfilters.md) | <code>ReturnType&lt;typeof aggFilters&gt;</code> | |
| [aggGeoBounds](./kibana-plugin-plugins-data-public.aggfunctionsmapping.agggeobounds.md) | <code>ReturnType&lt;typeof aggGeoBounds&gt;</code> | |
| [aggGeoCentroid](./kibana-plugin-plugins-data-public.aggfunctionsmapping.agggeocentroid.md) | <code>ReturnType&lt;typeof aggGeoCentroid&gt;</code> | |

View file

@ -20,6 +20,7 @@ export declare enum METRIC_TYPES
| COUNT | <code>&quot;count&quot;</code> | |
| CUMULATIVE\_SUM | <code>&quot;cumulative_sum&quot;</code> | |
| DERIVATIVE | <code>&quot;derivative&quot;</code> | |
| FILTERED\_METRIC | <code>&quot;filtered_metric&quot;</code> | |
| GEO\_BOUNDS | <code>&quot;geo_bounds&quot;</code> | |
| GEO\_CENTROID | <code>&quot;geo_centroid&quot;</code> | |
| MAX | <code>&quot;max&quot;</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) &gt; [AggFunctionsMapping](./kibana-plugin-plugins-data-server.aggfunctionsmapping.md) &gt; [aggFilteredMetric](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md)
## AggFunctionsMapping.aggFilteredMetric property
<b>Signature:</b>
```typescript
aggFilteredMetric: ReturnType<typeof aggFilteredMetric>;
```

View file

@ -28,6 +28,7 @@ export interface AggFunctionsMapping
| [aggDateRange](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggdaterange.md) | <code>ReturnType&lt;typeof aggDateRange&gt;</code> | |
| [aggDerivative](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggderivative.md) | <code>ReturnType&lt;typeof aggDerivative&gt;</code> | |
| [aggFilter](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilter.md) | <code>ReturnType&lt;typeof aggFilter&gt;</code> | |
| [aggFilteredMetric](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilteredmetric.md) | <code>ReturnType&lt;typeof aggFilteredMetric&gt;</code> | |
| [aggFilters](./kibana-plugin-plugins-data-server.aggfunctionsmapping.aggfilters.md) | <code>ReturnType&lt;typeof aggFilters&gt;</code> | |
| [aggGeoBounds](./kibana-plugin-plugins-data-server.aggfunctionsmapping.agggeobounds.md) | <code>ReturnType&lt;typeof aggGeoBounds&gt;</code> | |
| [aggGeoCentroid](./kibana-plugin-plugins-data-server.aggfunctionsmapping.agggeocentroid.md) | <code>ReturnType&lt;typeof aggGeoCentroid&gt;</code> | |

View file

@ -20,6 +20,7 @@ export declare enum METRIC_TYPES
| COUNT | <code>&quot;count&quot;</code> | |
| CUMULATIVE\_SUM | <code>&quot;cumulative_sum&quot;</code> | |
| DERIVATIVE | <code>&quot;derivative&quot;</code> | |
| FILTERED\_METRIC | <code>&quot;filtered_metric&quot;</code> | |
| GEO\_BOUNDS | <code>&quot;geo_bounds&quot;</code> | |
| GEO\_CENTROID | <code>&quot;geo_centroid&quot;</code> | |
| MAX | <code>&quot;max&quot;</code> | |

View file

@ -232,7 +232,9 @@ export class AggConfig {
const output = this.write(aggConfigs) as any;
const configDsl = {} as any;
configDsl[this.type.dslName || this.type.name] = output.params;
if (!this.type.hasNoDslParams) {
configDsl[this.type.dslName || this.type.name] = output.params;
}
// if the config requires subAggs, write them to the dsl as well
if (this.subAggs.length) {

View file

@ -21,7 +21,14 @@ import { TimeRange } from '../../../common';
function removeParentAggs(obj: any) {
for (const prop in obj) {
if (prop === 'parentAggs') delete obj[prop];
else if (typeof obj[prop] === 'object') removeParentAggs(obj[prop]);
else if (typeof obj[prop] === 'object') {
const hasParentAggsKey = 'parentAggs' in obj[prop];
removeParentAggs(obj[prop]);
// delete object if parentAggs was the last key
if (hasParentAggsKey && Object.keys(obj[prop]).length === 0) {
delete obj[prop];
}
}
}
}
@ -193,10 +200,12 @@ export class AggConfigs {
// advance the cursor and nest under the previous agg, or
// put it on the same level if the previous agg doesn't accept
// sub aggs
dslLvlCursor = prevDsl.aggs || dslLvlCursor;
dslLvlCursor = prevDsl?.aggs || dslLvlCursor;
}
const dsl = (dslLvlCursor[config.id] = config.toDsl(this));
const dsl = config.type.hasNoDslParams
? config.toDsl(this)
: (dslLvlCursor[config.id] = config.toDsl(this));
let subAggs: any;
parseParentAggs(dslLvlCursor, dsl);
@ -206,6 +215,11 @@ export class AggConfigs {
subAggs = dsl.aggs || (dsl.aggs = {});
}
if (subAggs) {
_.each(subAggs, (agg) => {
parseParentAggs(subAggs, agg);
});
}
if (subAggs && nestedMetrics) {
nestedMetrics.forEach((agg: any) => {
subAggs[agg.config.id] = agg.dsl;

View file

@ -32,6 +32,7 @@ export interface AggTypeConfig<
makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string);
ordered?: any;
hasNoDsl?: boolean;
hasNoDslParams?: boolean;
params?: Array<Partial<TParam>>;
valueType?: DatatableColumnType;
getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void);
@ -129,6 +130,12 @@ export class AggType<
* @type {Boolean}
*/
hasNoDsl: boolean;
/**
* Flag that prevents params from this aggregation from being included in the dsl. Sibling and parent aggs are still written.
*
* @type {Boolean}
*/
hasNoDslParams: boolean;
/**
* The method to create a filter representation of the bucket
* @param {object} aggConfig The instance of the aggConfig
@ -232,6 +239,7 @@ export class AggType<
this.makeLabel = config.makeLabel || constant(this.name);
this.ordered = config.ordered;
this.hasNoDsl = !!config.hasNoDsl;
this.hasNoDslParams = !!config.hasNoDslParams;
if (config.createFilter) {
this.createFilter = config.createFilter;

View file

@ -44,6 +44,7 @@ export const getAggTypes = () => ({
{ name: METRIC_TYPES.SUM_BUCKET, fn: metrics.getBucketSumMetricAgg },
{ name: METRIC_TYPES.MIN_BUCKET, fn: metrics.getBucketMinMetricAgg },
{ name: METRIC_TYPES.MAX_BUCKET, fn: metrics.getBucketMaxMetricAgg },
{ name: METRIC_TYPES.FILTERED_METRIC, fn: metrics.getFilteredMetricAgg },
{ name: METRIC_TYPES.GEO_BOUNDS, fn: metrics.getGeoBoundsMetricAgg },
{ name: METRIC_TYPES.GEO_CENTROID, fn: metrics.getGeoCentroidMetricAgg },
],
@ -80,6 +81,7 @@ export const getAggTypesFunctions = () => [
metrics.aggBucketMax,
metrics.aggBucketMin,
metrics.aggBucketSum,
metrics.aggFilteredMetric,
metrics.aggCardinality,
metrics.aggCount,
metrics.aggCumulativeSum,

View file

@ -97,6 +97,7 @@ describe('Aggs service', () => {
"sum_bucket",
"min_bucket",
"max_bucket",
"filtered_metric",
"geo_bounds",
"geo_centroid",
]
@ -142,6 +143,7 @@ describe('Aggs service', () => {
"sum_bucket",
"min_bucket",
"max_bucket",
"filtered_metric",
"geo_bounds",
"geo_centroid",
]

View file

@ -6,12 +6,15 @@
* Side Public License, v 1.
*/
import { cloneDeep } from 'lodash';
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types';
import { GeoBoundingBox } from './lib/geo_point';
import { aggFilterFnName } from './filter_fn';
import { BaseAggParams } from '../types';
import { Query } from '../../../types';
import { buildEsQuery, getEsQueryConfig } from '../../../es_query';
const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', {
defaultMessage: 'Filter',
@ -21,7 +24,7 @@ export interface AggParamsFilter extends BaseAggParams {
geo_bounding_box?: GeoBoundingBox;
}
export const getFilterBucketAgg = () =>
export const getFilterBucketAgg = ({ getConfig }: { getConfig: <T = any>(key: string) => any }) =>
new BucketAggType({
name: BUCKET_TYPES.FILTER,
expressionName: aggFilterFnName,
@ -31,5 +34,27 @@ export const getFilterBucketAgg = () =>
{
name: 'geo_bounding_box',
},
{
name: 'filter',
write(aggConfig, output) {
const filter: Query = aggConfig.params.filter;
const input = cloneDeep(filter);
if (!input) {
return;
}
const esQueryConfigs = getEsQueryConfig({ get: getConfig });
const query = buildEsQuery(aggConfig.getIndexPattern(), [input], [], esQueryConfigs);
if (!query) {
console.log('malformed filter agg params, missing "query" on input'); // eslint-disable-line no-console
return;
}
output.params = query;
},
},
],
});

View file

@ -23,6 +23,7 @@ describe('agg_expression_functions', () => {
"id": undefined,
"params": Object {
"customLabel": undefined,
"filter": undefined,
"geo_bounding_box": undefined,
"json": undefined,
},
@ -46,6 +47,7 @@ describe('agg_expression_functions', () => {
"id": undefined,
"params": Object {
"customLabel": undefined,
"filter": undefined,
"geo_bounding_box": Object {
"wkt": "BBOX (-74.1, -71.12, 40.73, 40.01)",
},
@ -57,6 +59,25 @@ describe('agg_expression_functions', () => {
`);
});
test('correctly parses filter string argument', () => {
const actual = fn({
filter: '{ "language": "kuery", "query": "a: b" }',
});
expect(actual.value.params.filter).toEqual({ language: 'kuery', query: 'a: b' });
});
test('errors out if geo_bounding_box is used together with filter', () => {
expect(() =>
fn({
filter: '{ "language": "kuery", "query": "a: b" }',
geo_bounding_box: JSON.stringify({
wkt: 'BBOX (-74.1, -71.12, 40.73, 40.01)',
}),
})
).toThrow();
});
test('correctly parses json string argument', () => {
const actual = fn({
json: '{ "foo": true }',

View file

@ -17,7 +17,7 @@ export const aggFilterFnName = 'aggFilter';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTER>;
type Arguments = Assign<AggArgs, { geo_bounding_box?: string }>;
type Arguments = Assign<AggArgs, { geo_bounding_box?: string; filter?: string }>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<
@ -59,6 +59,13 @@ export const aggFilter = (): FunctionDefinition => ({
defaultMessage: 'Filter results based on a point location within a bounding box',
}),
},
filter: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.filter.help', {
defaultMessage:
'Filter results based on a kql or lucene query. Do not use together with geo_bounding_box',
}),
},
json: {
types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.json.help', {
@ -75,6 +82,13 @@ export const aggFilter = (): FunctionDefinition => ({
fn: (input, args) => {
const { id, enabled, schema, ...rest } = args;
const geoBoundingBox = getParsedValue(args, 'geo_bounding_box');
const filter = getParsedValue(args, 'filter');
if (geoBoundingBox && filter) {
throw new Error("filter and geo_bounding_box can't be used together");
}
return {
type: 'agg_type',
value: {
@ -84,7 +98,8 @@ export const aggFilter = (): FunctionDefinition => ({
type: BUCKET_TYPES.FILTER,
params: {
...rest,
geo_bounding_box: getParsedValue(args, 'geo_bounding_box'),
geo_bounding_box: geoBoundingBox,
filter,
},
},
};

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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('filtered metric agg type', () => {
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.FILTERED_METRIC,
type: METRIC_TYPES.FILTERED_METRIC,
schema: 'metric',
params: {
customBucket: {
type: 'filter',
params: {
filter: { language: 'kuery', query: 'a: b' },
},
},
customMetric: {
type: 'cardinality',
params: {
field: 'bytes',
},
},
},
},
],
{
typesRegistry,
}
);
});
it('converts the response', () => {
const agg = aggConfigs.getResponseAggs()[0];
expect(
agg.getValue({
'filtered_metric-bucket': {
'filtered_metric-metric': {
value: 10,
},
},
})
).toEqual(10);
});
});

View file

@ -0,0 +1,56 @@
/*
* 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 { MetricAggType } from './metric_agg_type';
import { makeNestedLabel } from './lib/make_nested_label';
import { siblingPipelineAggHelper } from './lib/sibling_pipeline_agg_helper';
import { METRIC_TYPES } from './metric_agg_types';
import { AggConfigSerialized, BaseAggParams } from '../types';
import { aggFilteredMetricFnName } from './filtered_metric_fn';
export interface AggParamsFilteredMetric extends BaseAggParams {
customMetric?: AggConfigSerialized;
customBucket?: AggConfigSerialized;
}
const filteredMetricLabel = i18n.translate('data.search.aggs.metrics.filteredMetricLabel', {
defaultMessage: 'filtered',
});
const filteredMetricTitle = i18n.translate('data.search.aggs.metrics.filteredMetricTitle', {
defaultMessage: 'Filtered metric',
});
export const getFilteredMetricAgg = () => {
const { subtype, params, getSerializedFormat } = siblingPipelineAggHelper;
return new MetricAggType({
name: METRIC_TYPES.FILTERED_METRIC,
expressionName: aggFilteredMetricFnName,
title: filteredMetricTitle,
makeLabel: (agg) => makeNestedLabel(agg, filteredMetricLabel),
subtype,
params: [...params(['filter'])],
hasNoDslParams: true,
getSerializedFormat,
getValue(agg, bucket) {
const customMetric = agg.getParam('customMetric');
const customBucket = agg.getParam('customBucket');
return customMetric.getValue(bucket[customBucket.id]);
},
getValueBucketPath(agg) {
const customBucket = agg.getParam('customBucket');
const customMetric = agg.getParam('customMetric');
if (customMetric.type.name === 'count') {
return customBucket.getValueBucketPath();
}
return `${customBucket.getValueBucketPath()}>${customMetric.getValueBucketPath()}`;
},
});
};

View file

@ -0,0 +1,51 @@
/*
* 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 { functionWrapper } from '../test_helpers';
import { aggFilteredMetric } from './filtered_metric_fn';
describe('agg_expression_functions', () => {
describe('aggFilteredMetric', () => {
const fn = functionWrapper(aggFilteredMetric());
test('handles customMetric and customBucket as a subexpression', () => {
const actual = fn({
customMetric: fn({}),
customBucket: fn({}),
});
expect(actual.value.params).toMatchInlineSnapshot(`
Object {
"customBucket": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customBucket": undefined,
"customLabel": undefined,
"customMetric": undefined,
},
"schema": undefined,
"type": "filtered_metric",
},
"customLabel": undefined,
"customMetric": Object {
"enabled": true,
"id": undefined,
"params": Object {
"customBucket": undefined,
"customLabel": undefined,
"customMetric": undefined,
},
"schema": undefined,
"type": "filtered_metric",
},
}
`);
});
});
});

View file

@ -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 { Assign } from '@kbn/utility-types';
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../';
export const aggFilteredMetricFnName = 'aggFilteredMetric';
type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof METRIC_TYPES.FILTERED_METRIC>;
type Arguments = Assign<
AggArgs,
{ customBucket?: AggExpressionType; customMetric?: AggExpressionType }
>;
type Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition<
typeof aggFilteredMetricFnName,
Input,
Arguments,
Output
>;
export const aggFilteredMetric = (): FunctionDefinition => ({
name: aggFilteredMetricFnName,
help: i18n.translate('data.search.aggs.function.metrics.filtered_metric.help', {
defaultMessage: 'Generates a serialized agg config for a filtered metric agg',
}),
type: 'agg_type',
args: {
id: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.filtered_metric.id.help', {
defaultMessage: 'ID for this aggregation',
}),
},
enabled: {
types: ['boolean'],
default: true,
help: i18n.translate('data.search.aggs.metrics.filtered_metric.enabled.help', {
defaultMessage: 'Specifies whether this aggregation should be enabled',
}),
},
schema: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.filtered_metric.schema.help', {
defaultMessage: 'Schema to use for this aggregation',
}),
},
customBucket: {
types: ['agg_type'],
help: i18n.translate('data.search.aggs.metrics.filtered_metric.customBucket.help', {
defaultMessage:
'Agg config to use for building sibling pipeline aggregations. Has to be a filter aggregation',
}),
},
customMetric: {
types: ['agg_type'],
help: i18n.translate('data.search.aggs.metrics.filtered_metric.customMetric.help', {
defaultMessage: 'Agg config to use for building sibling pipeline aggregations',
}),
},
customLabel: {
types: ['string'],
help: i18n.translate('data.search.aggs.metrics.filtered_metric.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.FILTERED_METRIC,
params: {
...rest,
customBucket: args.customBucket?.value,
customMetric: args.customMetric?.value,
},
},
};
},
});

View file

@ -16,6 +16,8 @@ export * from './bucket_min_fn';
export * from './bucket_min';
export * from './bucket_sum_fn';
export * from './bucket_sum';
export * from './filtered_metric_fn';
export * from './filtered_metric';
export * from './cardinality_fn';
export * from './cardinality';
export * from './count';

View file

@ -21,6 +21,7 @@ const metricAggFilter = [
'!std_dev',
'!geo_bounds',
'!geo_centroid',
'!filtered_metric',
];
export const parentPipelineType = i18n.translate(

View file

@ -27,6 +27,7 @@ const metricAggFilter: string[] = [
'!cumulative_sum',
'!geo_bounds',
'!geo_centroid',
'!filtered_metric',
];
const bucketAggFilter: string[] = [];
@ -39,12 +40,12 @@ export const siblingPipelineType = i18n.translate(
export const siblingPipelineAggHelper = {
subtype: siblingPipelineType,
params() {
params(bucketFilter = bucketAggFilter) {
return [
{
name: 'customBucket',
type: 'agg',
allowedAggs: bucketAggFilter,
allowedAggs: bucketFilter,
default: null,
makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) {
const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false });
@ -69,7 +70,8 @@ export const siblingPipelineAggHelper = {
modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart(
'customMetric'
),
write: siblingPipelineAggWriter,
write: (agg: IMetricAggConfig, output: Record<string, any>) =>
siblingPipelineAggWriter(agg, output),
},
] as Array<MetricAggParam<IMetricAggConfig>>;
},

View file

@ -8,6 +8,7 @@
export enum METRIC_TYPES {
AVG = 'avg',
FILTERED_METRIC = 'filtered_metric',
CARDINALITY = 'cardinality',
AVG_BUCKET = 'avg_bucket',
MAX_BUCKET = 'max_bucket',

View file

@ -41,6 +41,7 @@ import {
AggParamsBucketMax,
AggParamsBucketMin,
AggParamsBucketSum,
AggParamsFilteredMetric,
AggParamsCardinality,
AggParamsCumulativeSum,
AggParamsDateHistogram,
@ -84,6 +85,7 @@ import {
getCalculateAutoTimeExpression,
METRIC_TYPES,
AggConfig,
aggFilteredMetric,
} from './';
export { IAggConfig, AggConfigSerialized } from './agg_config';
@ -188,6 +190,7 @@ export interface AggParamsMapping {
[METRIC_TYPES.MAX_BUCKET]: AggParamsBucketMax;
[METRIC_TYPES.MIN_BUCKET]: AggParamsBucketMin;
[METRIC_TYPES.SUM_BUCKET]: AggParamsBucketSum;
[METRIC_TYPES.FILTERED_METRIC]: AggParamsFilteredMetric;
[METRIC_TYPES.CUMULATIVE_SUM]: AggParamsCumulativeSum;
[METRIC_TYPES.DERIVATIVE]: AggParamsDerivative;
[METRIC_TYPES.MOVING_FN]: AggParamsMovingAvg;
@ -217,6 +220,7 @@ export interface AggFunctionsMapping {
aggBucketMax: ReturnType<typeof aggBucketMax>;
aggBucketMin: ReturnType<typeof aggBucketMin>;
aggBucketSum: ReturnType<typeof aggBucketSum>;
aggFilteredMetric: ReturnType<typeof aggFilteredMetric>;
aggCardinality: ReturnType<typeof aggCardinality>;
aggCount: ReturnType<typeof aggCount>;
aggCumulativeSum: ReturnType<typeof aggCumulativeSum>;

View file

@ -329,6 +329,10 @@ export interface AggFunctionsMapping {
//
// (undocumented)
aggFilter: ReturnType<typeof aggFilter>;
// Warning: (ae-forgotten-export) The symbol "aggFilteredMetric" needs to be exported by the entry point index.d.ts
//
// (undocumented)
aggFilteredMetric: ReturnType<typeof aggFilteredMetric>;
// Warning: (ae-forgotten-export) The symbol "aggFilters" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -711,7 +715,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_34, Arguments_20, Output_34>;
export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>;
// 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
@ -720,7 +724,7 @@ export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'e
// Warning: (ae-missing-release-tag) "EsdslExpressionFunctionDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2<typeof name_3, Input_35, Arguments_22, Output_35>;
export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition_2<typeof name_3, Input_36, Arguments_23, 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)
//
@ -861,7 +865,7 @@ export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', Ex
// Warning: (ae-missing-release-tag) "ExpressionFunctionKibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_21, Promise<KibanaContext>, ExecutionContext<Adapters_2, ExecutionContextSearch>>;
export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_22, Promise<KibanaContext>, ExecutionContext<Adapters_2, ExecutionContextSearch>>;
// Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -1838,6 +1842,8 @@ export enum METRIC_TYPES {
// (undocumented)
DERIVATIVE = "derivative",
// (undocumented)
FILTERED_METRIC = "filtered_metric",
// (undocumented)
GEO_BOUNDS = "geo_bounds",
// (undocumented)
GEO_CENTROID = "geo_centroid",
@ -2657,7 +2663,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:139: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:141: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

View file

@ -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(21);
expect(start.types.getAll().metrics.length).toBe(22);
});
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(22);
expect(start.types.getAll().metrics.length).toBe(23);
expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true);
});
});

View file

@ -134,6 +134,10 @@ export interface AggFunctionsMapping {
//
// (undocumented)
aggFilter: ReturnType<typeof aggFilter>;
// Warning: (ae-forgotten-export) The symbol "aggFilteredMetric" needs to be exported by the entry point index.d.ts
//
// (undocumented)
aggFilteredMetric: ReturnType<typeof aggFilteredMetric>;
// Warning: (ae-forgotten-export) The symbol "aggFilters" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -405,7 +409,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_34, Arguments_20, Output_34>;
export type EsaggsExpressionFunctionDefinition = ExpressionFunctionDefinition<'esaggs', Input_35, Arguments_21, Output_35>;
// Warning: (ae-missing-release-tag) "esFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -485,7 +489,7 @@ export type ExpressionFunctionKibana = ExpressionFunctionDefinition<'kibana', Ex
// Warning: (ae-missing-release-tag) "ExpressionFunctionKibanaContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_21, Promise<KibanaContext>, ExecutionContext<Adapters, ExecutionContextSearch>>;
export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<'kibana_context', KibanaContext | null, Arguments_22, Promise<KibanaContext>, ExecutionContext<Adapters, ExecutionContextSearch>>;
// Warning: (ae-missing-release-tag) "ExpressionValueSearchContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@ -1147,6 +1151,8 @@ export enum METRIC_TYPES {
// (undocumented)
DERIVATIVE = "derivative",
// (undocumented)
FILTERED_METRIC = "filtered_metric",
// (undocumented)
GEO_BOUNDS = "geo_bounds",
// (undocumented)
GEO_CENTROID = "geo_centroid",

View file

@ -63,6 +63,7 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition<VisParams> =>
'!moving_avg',
'!cumulative_sum',
'!geo_bounds',
'!filtered_metric',
],
aggSettings: {
top_hits: {

View file

@ -50,7 +50,7 @@ export const tableVisLegacyTypeDefinition: VisTypeDefinition<TableVisParams> = {
title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', {
defaultMessage: 'Metric',
}),
aggFilter: ['!geo_centroid', '!geo_bounds'],
aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'],
aggSettings: {
top_hits: {
allowStrings: true,

View file

@ -46,7 +46,7 @@ export const tableVisTypeDefinition: VisTypeDefinition<TableVisParams> = {
title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', {
defaultMessage: 'Metric',
}),
aggFilter: ['!geo_centroid', '!geo_bounds'],
aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'],
aggSettings: {
top_hits: {
allowStrings: true,

View file

@ -51,6 +51,7 @@ export const tagCloudVisTypeDefinition = {
'!derivative',
'!geo_bounds',
'!geo_centroid',
'!filtered_metric',
],
defaults: [{ schema: 'metric', type: 'count' }],
},

View file

@ -119,6 +119,7 @@ export const gaugeVisTypeDefinition: VisTypeDefinition<GaugeVisParams> = {
'!moving_avg',
'!cumulative_sum',
'!geo_bounds',
'!filtered_metric',
],
defaults: [{ schema: 'metric', type: 'count' }],
},

View file

@ -83,6 +83,7 @@ export const goalVisTypeDefinition: VisTypeDefinition<GaugeVisParams> = {
'!moving_avg',
'!cumulative_sum',
'!geo_bounds',
'!filtered_metric',
],
defaults: [{ schema: 'metric', type: 'count' }],
},

View file

@ -94,6 +94,7 @@ export const heatmapVisTypeDefinition: VisTypeDefinition<HeatmapVisParams> = {
'cardinality',
'std_dev',
'top_hits',
'!filtered_metric',
],
defaults: [{ schema: 'metric', type: 'count' }],
},

View file

@ -133,7 +133,7 @@ export const getAreaVisTypeDefinition = (
title: i18n.translate('visTypeXy.area.metricsTitle', {
defaultMessage: 'Y-axis',
}),
aggFilter: ['!geo_centroid', '!geo_bounds'],
aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'],
min: 1,
defaults: [{ schema: 'metric', type: 'count' }],
},

View file

@ -137,7 +137,7 @@ export const getHistogramVisTypeDefinition = (
defaultMessage: 'Y-axis',
}),
min: 1,
aggFilter: ['!geo_centroid', '!geo_bounds'],
aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'],
defaults: [{ schema: 'metric', type: 'count' }],
},
{

View file

@ -136,7 +136,7 @@ export const getHorizontalBarVisTypeDefinition = (
defaultMessage: 'Y-axis',
}),
min: 1,
aggFilter: ['!geo_centroid', '!geo_bounds'],
aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'],
defaults: [{ schema: 'metric', type: 'count' }],
},
{

View file

@ -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'],
aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'],
defaults: [{ schema: 'metric', type: 'count' }],
},
{

View file

@ -0,0 +1,82 @@
/*
* 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 { EuiLink, EuiText, EuiPopover, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
export function AdvancedOptions(props: {
options: Array<{
title: string;
dataTestSubj: string;
onClick: () => void;
showInPopover: boolean;
inlineElement: React.ReactElement | null;
}>;
}) {
const [popoverOpen, setPopoverOpen] = useState(false);
const popoverOptions = props.options.filter((option) => option.showInPopover);
const inlineOptions = props.options
.filter((option) => option.inlineElement)
.map((option) => React.cloneElement(option.inlineElement!, { key: option.dataTestSubj }));
return (
<>
{popoverOptions.length > 0 && (
<EuiText textAlign="right">
<EuiSpacer size="s" />
<EuiPopover
ownFocus
button={
<EuiButtonEmpty
size="xs"
iconType="arrowDown"
iconSide="right"
data-test-subj="indexPattern-advanced-popover"
onClick={() => {
setPopoverOpen(!popoverOpen);
}}
>
{i18n.translate('xpack.lens.indexPattern.advancedSettings', {
defaultMessage: 'Add advanced options',
})}
</EuiButtonEmpty>
}
isOpen={popoverOpen}
closePopover={() => {
setPopoverOpen(false);
}}
>
{popoverOptions.map(({ dataTestSubj, onClick, title }, index) => (
<React.Fragment key={dataTestSubj}>
<EuiText size="s">
<EuiLink
data-test-subj={dataTestSubj}
color="text"
onClick={() => {
setPopoverOpen(false);
onClick();
}}
>
{title}
</EuiLink>
</EuiText>
{popoverOptions.length - 1 !== index && <EuiSpacer size="s" />}
</React.Fragment>
))}
</EuiPopover>
</EuiText>
)}
{inlineOptions.length > 0 && (
<>
<EuiSpacer size="s" />
{inlineOptions}
</>
)}
</>
);
}

View file

@ -32,6 +32,7 @@ import {
resetIncomplete,
FieldBasedIndexPatternColumn,
canTransition,
DEFAULT_TIME_SCALE,
} from '../operations';
import { mergeLayer } from '../state_helpers';
import { FieldSelect } from './field_select';
@ -41,7 +42,9 @@ import { IndexPattern, IndexPatternLayer } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
import { FormatSelector } from './format_selector';
import { ReferenceEditor } from './reference_editor';
import { TimeScaling } from './time_scaling';
import { setTimeScaling, TimeScaling } from './time_scaling';
import { defaultFilter, Filtering, setFilter } from './filtering';
import { AdvancedOptions } from './advanced_options';
const operationPanels = getOperationDisplay();
@ -156,6 +159,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
.filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type));
}, [fieldByOperation, operationWithoutField]);
const [filterByOpenInitially, setFilterByOpenInitally] = useState(false);
// Operations are compatible if they match inputs. They are always compatible in
// the empty state. Field-based operations are not compatible with field-less operations.
const operationsWithCompatibility = [...possibleOperations].map((operationType) => {
@ -458,11 +463,63 @@ export function DimensionEditor(props: DimensionEditorProps) {
)}
{!currentFieldIsInvalid && !incompleteInfo && selectedColumn && (
<TimeScaling
selectedColumn={selectedColumn}
columnId={columnId}
layer={state.layers[layerId]}
updateLayer={setStateWrapper}
<AdvancedOptions
options={[
{
title: i18n.translate('xpack.lens.indexPattern.timeScale.enableTimeScale', {
defaultMessage: 'Normalize by unit',
}),
dataTestSubj: 'indexPattern-time-scaling-enable',
onClick: () => {
setStateWrapper(
setTimeScaling(columnId, state.layers[layerId], DEFAULT_TIME_SCALE)
);
},
showInPopover: Boolean(
operationDefinitionMap[selectedColumn.operationType].timeScalingMode &&
operationDefinitionMap[selectedColumn.operationType].timeScalingMode !==
'disabled' &&
Object.values(state.layers[layerId].columns).some(
(col) => col.operationType === 'date_histogram'
) &&
!selectedColumn.timeScale
),
inlineElement: (
<TimeScaling
selectedColumn={selectedColumn}
columnId={columnId}
layer={state.layers[layerId]}
updateLayer={setStateWrapper}
/>
),
},
{
title: i18n.translate('xpack.lens.indexPattern.filterBy.label', {
defaultMessage: 'Filter by',
}),
dataTestSubj: 'indexPattern-filter-by-enable',
onClick: () => {
setFilterByOpenInitally(true);
setStateWrapper(setFilter(columnId, state.layers[layerId], defaultFilter));
},
showInPopover: Boolean(
operationDefinitionMap[selectedColumn.operationType].filterable &&
!selectedColumn.filter
),
inlineElement:
operationDefinitionMap[selectedColumn.operationType].filterable &&
selectedColumn.filter ? (
<Filtering
indexPattern={currentIndexPattern}
selectedColumn={selectedColumn}
columnId={columnId}
layer={state.layers[layerId]}
updateLayer={setStateWrapper}
isInitiallyOpen={filterByOpenInitially}
/>
) : null,
},
]}
/>
)}
</div>

View file

@ -6,7 +6,7 @@
*/
import { ReactWrapper, ShallowWrapper } from 'enzyme';
import React, { ChangeEvent, MouseEvent } from 'react';
import React, { ChangeEvent, MouseEvent, ReactElement } from 'react';
import { act } from 'react-dom/test-utils';
import {
EuiComboBox,
@ -15,6 +15,7 @@ import {
EuiRange,
EuiSelect,
EuiButtonIcon,
EuiPopover,
} from '@elastic/eui';
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
import {
@ -30,10 +31,14 @@ import { documentField } from '../document_field';
import { OperationMetadata } from '../../types';
import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram';
import { getFieldByNameFactory } from '../pure_helpers';
import { TimeScaling } from './time_scaling';
import { DimensionEditor } from './dimension_editor';
import { AdvancedOptions } from './advanced_options';
import { Filtering } from './filtering';
jest.mock('../loader');
jest.mock('../query_input', () => ({
QueryInput: () => null,
}));
jest.mock('../operations');
jest.mock('lodash', () => {
const original = jest.requireActual('lodash');
@ -1029,7 +1034,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
}
it('should not show custom options if time scaling is not available', () => {
wrapper = mount(
wrapper = shallow(
<IndexPatternDimensionEditorComponent
{...getProps({
operationType: 'avg',
@ -1037,17 +1042,26 @@ describe('IndexPatternDimensionEditorPanel', () => {
})}
/>
);
expect(wrapper.find('[data-test-subj="indexPattern-time-scaling"]')).toHaveLength(0);
expect(
wrapper
.find(DimensionEditor)
.dive()
.find(AdvancedOptions)
.dive()
.find('[data-test-subj="indexPattern-time-scaling-enable"]')
).toHaveLength(0);
});
it('should show custom options if time scaling is available', () => {
wrapper = mount(<IndexPatternDimensionEditorComponent {...getProps({})} />);
wrapper = shallow(<IndexPatternDimensionEditorComponent {...getProps({})} />);
expect(
wrapper
.find(TimeScaling)
.find('[data-test-subj="indexPattern-time-scaling-popover"]')
.exists()
).toBe(true);
.find(DimensionEditor)
.dive()
.find(AdvancedOptions)
.dive()
.find('[data-test-subj="indexPattern-time-scaling-enable"]')
).toHaveLength(1);
});
it('should show current time scaling if set', () => {
@ -1066,7 +1080,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
wrapper
.find(DimensionEditor)
.dive()
.find(TimeScaling)
.find(AdvancedOptions)
.dive()
.find('[data-test-subj="indexPattern-time-scaling-enable"]')
.prop('onClick')!({} as MouseEvent);
@ -1239,6 +1253,199 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
});
describe('filtering', () => {
function getProps(colOverrides: Partial<IndexPatternColumn>) {
return {
...defaultProps,
state: getStateWithColumns({
datecolumn: {
dataType: 'date',
isBucketed: true,
label: '',
customLabel: true,
operationType: 'date_histogram',
sourceField: 'ts',
params: {
interval: '1d',
},
},
col2: {
dataType: 'number',
isBucketed: false,
label: 'Count of records',
operationType: 'count',
sourceField: 'Records',
...colOverrides,
} as IndexPatternColumn,
}),
columnId: 'col2',
};
}
it('should not show custom options if time scaling is not available', () => {
wrapper = shallow(
<IndexPatternDimensionEditorComponent
{...getProps({
operationType: 'terms',
sourceField: 'bytes',
})}
/>
);
expect(
wrapper
.find(DimensionEditor)
.dive()
.find(AdvancedOptions)
.dive()
.find('[data-test-subj="indexPattern-filter-by-enable"]')
).toHaveLength(0);
});
it('should show custom options if filtering is available', () => {
wrapper = shallow(<IndexPatternDimensionEditorComponent {...getProps({})} />);
expect(
wrapper
.find(DimensionEditor)
.dive()
.find(AdvancedOptions)
.dive()
.find('[data-test-subj="indexPattern-filter-by-enable"]')
).toHaveLength(1);
});
it('should show current filter if set', () => {
wrapper = mount(
<IndexPatternDimensionEditorComponent
{...getProps({ filter: { language: 'kuery', query: 'a: b' } })}
/>
);
expect(
(wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.value
).toEqual({ language: 'kuery', query: 'a: b' });
});
it('should allow to set filter initially', () => {
const props = getProps({});
wrapper = shallow(<IndexPatternDimensionEditorComponent {...props} />);
wrapper
.find(DimensionEditor)
.dive()
.find(AdvancedOptions)
.dive()
.find('[data-test-subj="indexPattern-filter-by-enable"]')
.prop('onClick')!({} as MouseEvent);
expect(props.setState).toHaveBeenCalledWith(
{
...props.state,
layers: {
first: {
...props.state.layers.first,
columns: {
...props.state.layers.first.columns,
col2: expect.objectContaining({
filter: {
language: 'kuery',
query: '',
},
}),
},
},
},
},
{ shouldRemoveDimension: false, shouldReplaceDimension: true }
);
});
it('should carry over filter to other operation if possible', () => {
const props = getProps({
filter: { language: 'kuery', query: 'a: b' },
sourceField: 'bytes',
operationType: 'sum',
label: 'Sum of bytes per hour',
});
wrapper = mount(<IndexPatternDimensionEditorComponent {...props} />);
wrapper
.find('button[data-test-subj="lns-indexPatternDimension-count incompatible"]')
.simulate('click');
expect(props.setState).toHaveBeenCalledWith(
{
...props.state,
layers: {
first: {
...props.state.layers.first,
columns: {
...props.state.layers.first.columns,
col2: expect.objectContaining({
filter: { language: 'kuery', query: 'a: b' },
}),
},
},
},
},
{ shouldRemoveDimension: false, shouldReplaceDimension: true }
);
});
it('should allow to change filter', () => {
const props = getProps({
filter: { language: 'kuery', query: 'a: b' },
});
wrapper = mount(<IndexPatternDimensionEditorComponent {...props} />);
(wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.onChange({
language: 'kuery',
query: 'c: d',
});
expect(props.setState).toHaveBeenCalledWith(
{
...props.state,
layers: {
first: {
...props.state.layers.first,
columns: {
...props.state.layers.first.columns,
col2: expect.objectContaining({
filter: { language: 'kuery', query: 'c: d' },
}),
},
},
},
},
{ shouldRemoveDimension: false, shouldReplaceDimension: true }
);
});
it('should allow to remove filter', () => {
const props = getProps({
filter: { language: 'kuery', query: 'a: b' },
});
wrapper = mount(<IndexPatternDimensionEditorComponent {...props} />);
wrapper
.find('[data-test-subj="indexPattern-filter-by-remove"]')
.find(EuiButtonIcon)
.prop('onClick')!(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{} as any
);
expect(props.setState).toHaveBeenCalledWith(
{
...props.state,
layers: {
first: {
...props.state.layers.first,
columns: {
...props.state.layers.first.columns,
col2: expect.objectContaining({
filter: undefined,
}),
},
},
},
},
{ shouldRemoveDimension: false, shouldReplaceDimension: true }
);
});
});
it('should render invalid field if field reference is broken', () => {
wrapper = mount(
<IndexPatternDimensionEditorComponent

View file

@ -0,0 +1,131 @@
/*
* 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 { EuiButtonIcon, EuiLink, EuiPanel, EuiPopover } from '@elastic/eui';
import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { Query } from 'src/plugins/data/public';
import { IndexPatternColumn, operationDefinitionMap } from '../operations';
import { isQueryValid } from '../operations/definitions/filters';
import { QueryInput } from '../query_input';
import { IndexPattern, IndexPatternLayer } from '../types';
// to do: get the language from uiSettings
export const defaultFilter: Query = {
query: '',
language: 'kuery',
};
export function setFilter(columnId: string, layer: IndexPatternLayer, query: Query | undefined) {
return {
...layer,
columns: {
...layer.columns,
[columnId]: {
...layer.columns[columnId],
filter: query,
},
},
};
}
export function Filtering({
selectedColumn,
columnId,
layer,
updateLayer,
indexPattern,
isInitiallyOpen,
}: {
selectedColumn: IndexPatternColumn;
indexPattern: IndexPattern;
columnId: string;
layer: IndexPatternLayer;
updateLayer: (newLayer: IndexPatternLayer) => void;
isInitiallyOpen: boolean;
}) {
const [filterPopoverOpen, setFilterPopoverOpen] = useState(isInitiallyOpen);
const selectedOperation = operationDefinitionMap[selectedColumn.operationType];
if (!selectedOperation.filterable || !selectedColumn.filter) {
return null;
}
const isInvalid = !isQueryValid(selectedColumn.filter, indexPattern);
return (
<EuiFormRow
display="columnCompressed"
fullWidth
label={i18n.translate('xpack.lens.indexPattern.filterBy.label', {
defaultMessage: 'Filter by',
})}
>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiPopover
isOpen={filterPopoverOpen}
closePopover={() => {
setFilterPopoverOpen(false);
}}
anchorClassName="eui-fullWidth"
panelClassName="lnsIndexPatternDimensionEditor__filtersEditor"
button={
<EuiPanel paddingSize="none">
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>{/* Empty for spacing */}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
className="lnsFiltersOperation__popoverButton"
data-test-subj="indexPattern-filters-existingFilterTrigger"
onClick={() => {
setFilterPopoverOpen(!filterPopoverOpen);
}}
color={isInvalid ? 'danger' : 'text'}
title={i18n.translate('xpack.lens.indexPattern.filterBy.clickToEdit', {
defaultMessage: 'Click to edit',
})}
>
{selectedColumn.filter.query ||
i18n.translate('xpack.lens.indexPattern.filterBy.emptyFilterQuery', {
defaultMessage: '(empty)',
})}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
}
>
<QueryInput
indexPattern={indexPattern}
data-test-subj="indexPattern-filter-by-input"
value={selectedColumn.filter || defaultFilter}
onChange={(newQuery) => {
updateLayer(setFilter(columnId, layer, newQuery));
}}
isInvalid={false}
onSubmit={() => {}}
/>
</EuiPopover>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-test-subj="indexPattern-filter-by-remove"
color="danger"
aria-label={i18n.translate('xpack.lens.filterBy.removeLabel', {
defaultMessage: 'Remove filter',
})}
onClick={() => {
updateLayer(setFilter(columnId, layer, undefined));
}}
iconType="cross"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
);
}

View file

@ -7,23 +7,11 @@
import { EuiToolTip } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui';
import {
EuiLink,
EuiFormRow,
EuiSelect,
EuiFlexItem,
EuiFlexGroup,
EuiButtonIcon,
EuiText,
EuiPopover,
EuiButtonEmpty,
EuiSpacer,
} from '@elastic/eui';
import { EuiFormRow, EuiSelect, EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import React from 'react';
import {
adjustTimeScaleLabelSuffix,
DEFAULT_TIME_SCALE,
IndexPatternColumn,
operationDefinitionMap,
} from '../operations';
@ -64,7 +52,6 @@ export function TimeScaling({
layer: IndexPatternLayer;
updateLayer: (newLayer: IndexPatternLayer) => void;
}) {
const [popoverOpen, setPopoverOpen] = useState(false);
const hasDateHistogram = layer.columnOrder.some(
(colId) => layer.columns[colId].operationType === 'date_histogram'
);
@ -72,56 +59,12 @@ export function TimeScaling({
if (
!selectedOperation.timeScalingMode ||
selectedOperation.timeScalingMode === 'disabled' ||
!hasDateHistogram
!hasDateHistogram ||
!selectedColumn.timeScale
) {
return null;
}
if (!selectedColumn.timeScale) {
return (
<EuiText textAlign="right">
<EuiSpacer size="s" />
<EuiPopover
ownFocus
button={
<EuiButtonEmpty
size="xs"
iconType="arrowDown"
iconSide="right"
data-test-subj="indexPattern-time-scaling-popover"
onClick={() => {
setPopoverOpen(!popoverOpen);
}}
>
{i18n.translate('xpack.lens.indexPattern.timeScale.advancedSettings', {
defaultMessage: 'Add advanced options',
})}
</EuiButtonEmpty>
}
isOpen={popoverOpen}
closePopover={() => {
setPopoverOpen(false);
}}
>
<EuiText size="s">
<EuiLink
data-test-subj="indexPattern-time-scaling-enable"
color="text"
onClick={() => {
setPopoverOpen(false);
updateLayer(setTimeScaling(columnId, layer, DEFAULT_TIME_SCALE));
}}
>
{i18n.translate('xpack.lens.indexPattern.timeScale.enableTimeScale', {
defaultMessage: 'Normalize by unit',
})}
</EuiLink>
</EuiText>
</EuiPopover>
</EuiText>
);
}
return (
<EuiFormRow
display="columnCompressed"

View file

@ -521,6 +521,123 @@ describe('IndexPattern Data Source', () => {
]);
});
it('should wrap filtered metrics in filtered metric aggregation', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['col1', 'col2', 'col3'],
columns: {
col1: {
label: 'Count of records',
dataType: 'number',
isBucketed: false,
sourceField: 'Records',
operationType: 'count',
timeScale: 'h',
filter: {
language: 'kuery',
query: 'bytes > 5',
},
},
col2: {
label: 'Average of bytes',
dataType: 'number',
isBucketed: false,
sourceField: 'bytes',
operationType: 'avg',
timeScale: 'h',
},
col3: {
label: 'Date',
dataType: 'date',
isBucketed: true,
operationType: 'date_histogram',
sourceField: 'timestamp',
params: {
interval: 'auto',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(ast.chain[0].arguments.aggs[0]).toMatchInlineSnapshot(`
Object {
"chain": Array [
Object {
"arguments": Object {
"customBucket": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"filter": Array [
"{\\"language\\":\\"kuery\\",\\"query\\":\\"bytes > 5\\"}",
],
"id": Array [
"col1-filter",
],
"schema": Array [
"bucket",
],
},
"function": "aggFilter",
"type": "function",
},
],
"type": "expression",
},
],
"customMetric": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"enabled": Array [
true,
],
"id": Array [
"col1-metric",
],
"schema": Array [
"metric",
],
},
"function": "aggCount",
"type": "function",
},
],
"type": "expression",
},
],
"enabled": Array [
true,
],
"id": Array [
"col1",
],
"schema": Array [
"metric",
],
},
"function": "aggFilteredMetric",
"type": "function",
},
],
"type": "expression",
}
`);
});
it('should add time_scale and format function if time scale is set and supported', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',

View file

@ -82,6 +82,7 @@ export const counterRateOperation: OperationDefinition<
scale: 'ratio',
references: referenceIds,
timeScale,
filter: previousColumn?.filter,
params: getFormatFromPreviousColumn(previousColumn),
};
},
@ -106,4 +107,5 @@ export const counterRateOperation: OperationDefinition<
)?.join(', ');
},
timeScalingMode: 'mandatory',
filterable: true,
};

View file

@ -77,6 +77,7 @@ export const cumulativeSumOperation: OperationDefinition<
operationType: 'cumulative_sum',
isBucketed: false,
scale: 'ratio',
filter: previousColumn?.filter,
references: referenceIds,
params: getFormatFromPreviousColumn(previousColumn),
};
@ -101,4 +102,5 @@ export const cumulativeSumOperation: OperationDefinition<
})
)?.join(', ');
},
filterable: true,
};

View file

@ -83,6 +83,7 @@ export const derivativeOperation: OperationDefinition<
scale: 'ratio',
references: referenceIds,
timeScale: previousColumn?.timeScale,
filter: previousColumn?.filter,
params: getFormatFromPreviousColumn(previousColumn),
};
},
@ -108,4 +109,5 @@ export const derivativeOperation: OperationDefinition<
)?.join(', ');
},
timeScalingMode: 'optional',
filterable: true,
};

View file

@ -89,6 +89,7 @@ export const movingAverageOperation: OperationDefinition<
scale: 'ratio',
references: referenceIds,
timeScale: previousColumn?.timeScale,
filter: previousColumn?.filter,
params: {
window: 5,
...getFormatFromPreviousColumn(previousColumn),
@ -119,6 +120,7 @@ export const movingAverageOperation: OperationDefinition<
)?.join(', ');
},
timeScalingMode: 'optional',
filterable: true,
};
function MovingAverageParamEditor({

View file

@ -70,6 +70,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
(!newField.aggregationRestrictions || newField.aggregationRestrictions.cardinality)
);
},
filterable: true,
getDefaultLabel: (column, indexPattern) => ofName(getSafeName(column.sourceField, indexPattern)),
buildColumn({ field, previousColumn }) {
return {
@ -79,6 +80,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
scale: SCALE,
sourceField: field.name,
isBucketed: IS_BUCKETED,
filter: previousColumn?.filter,
params: getFormatFromPreviousColumn(previousColumn),
};
},

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { Query } from 'src/plugins/data/public';
import type { Operation } from '../../../types';
import { TimeScaleUnit } from '../../time_scale';
import type { OperationType } from '../definitions';
@ -14,6 +15,7 @@ export interface BaseIndexPatternColumn extends Operation {
operationType: string;
customLabel?: boolean;
timeScale?: TimeScaleUnit;
filter?: Query;
}
// Formatting can optionally be added to any column

View file

@ -61,6 +61,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
scale: 'ratio',
sourceField: field.name,
timeScale: previousColumn?.timeScale,
filter: previousColumn?.filter,
params:
previousColumn?.dataType === 'number' &&
previousColumn.params &&
@ -87,4 +88,5 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
return true;
},
timeScalingMode: 'optional',
filterable: true,
};

View file

@ -10,8 +10,9 @@ import { shallow, mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { EuiPopover, EuiLink } from '@elastic/eui';
import { createMockedIndexPattern } from '../../../mocks';
import { FilterPopover, QueryInput } from './filter_popover';
import { FilterPopover } from './filter_popover';
import { LabelInput } from '../shared_components';
import { QueryInput } from '../../../query_input';
jest.mock('.', () => ({
isQueryValid: () => true,

View file

@ -8,13 +8,12 @@
import './filter_popover.scss';
import React, { MouseEventHandler, useEffect, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { EuiPopover, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilterValue, defaultLabel, isQueryValid } from '.';
import { IndexPattern } from '../../../types';
import { QueryStringInput, Query } from '../../../../../../../../src/plugins/data/public';
import { Query } from '../../../../../../../../src/plugins/data/public';
import { LabelInput } from '../shared_components';
import { QueryInput } from '../../../query_input';
export const FilterPopover = ({
filter,
@ -94,54 +93,3 @@ export const FilterPopover = ({
</EuiPopover>
);
};
export const QueryInput = ({
value,
onChange,
indexPattern,
isInvalid,
onSubmit,
}: {
value: Query;
onChange: (input: Query) => void;
indexPattern: IndexPattern;
isInvalid: boolean;
onSubmit: () => void;
}) => {
const [inputValue, setInputValue] = useState(value);
useDebounce(() => onChange(inputValue), 256, [inputValue]);
const handleInputChange = (input: Query) => {
setInputValue(input);
};
return (
<QueryStringInput
dataTestSubj="indexPattern-filters-queryStringInput"
size="s"
isInvalid={isInvalid}
bubbleSubmitEvent={false}
indexPatterns={[indexPattern]}
query={inputValue}
onChange={handleInputChange}
onSubmit={() => {
if (inputValue.query) {
onSubmit();
}
}}
placeholder={
inputValue.language === 'kuery'
? i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderKql', {
defaultMessage: '{example}',
values: { example: 'method : "GET" or status : "404"' },
})
: i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderLucene', {
defaultMessage: '{example}',
values: { example: 'method:GET OR status:404' },
})
}
languageSwitcherPopoverAnchorPosition="rightDown"
/>
);
};

View file

@ -230,6 +230,7 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn> {
* If set to optional, time scaling won't be enabled by default and can be removed.
*/
timeScalingMode?: TimeScalingMode;
filterable?: boolean;
getHelpMessage?: (props: HelpProps<C>) => React.ReactNode;
}

View file

@ -161,12 +161,14 @@ export const lastValueOperation: OperationDefinition<LastValueIndexPatternColumn
isBucketed: false,
scale: field.type === 'string' ? 'ordinal' : 'ratio',
sourceField: field.name,
filter: previousColumn?.filter,
params: {
sortField,
...getFormatFromPreviousColumn(previousColumn),
},
};
},
filterable: true,
toEsAggsFn: (column, columnId) => {
return buildExpressionFunction<AggFunctionsMapping['aggTopHit']>('aggTopHit', {
id: columnId,

View file

@ -99,6 +99,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
isBucketed: false,
scale: 'ratio',
timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined,
filter: previousColumn?.filter,
params: getFormatFromPreviousColumn(previousColumn),
} as T),
onFieldChange: (oldColumn, field) => {
@ -118,6 +119,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
},
getErrorMessage: (layer, columnId, indexPattern) =>
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
filterable: true,
} as OperationDefinition<T, 'field'>;
}

View file

@ -967,6 +967,49 @@ describe('state_helpers', () => {
);
});
it('should remove filter from the wrapped column if it gets wrapped (case new1)', () => {
const expectedColumn = {
label: 'Count',
customLabel: true,
dataType: 'number' as const,
isBucketed: false,
sourceField: 'Records',
operationType: 'count' as const,
};
const testFilter = { language: 'kuery', query: '' };
const layer: IndexPatternLayer = {
indexPatternId: '1',
columnOrder: ['col1'],
columns: { col1: { ...expectedColumn, filter: testFilter } },
};
const result = replaceColumn({
layer,
indexPattern,
columnId: 'col1',
op: 'testReference' as OperationType,
visualizationGroups: [],
});
expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith(
expect.objectContaining({
referenceIds: ['id1'],
previousColumn: expect.objectContaining({
// filter should be passed to the buildColumn function of the target operation
filter: testFilter,
}),
})
);
expect(result.columns).toEqual(
expect.objectContaining({
// filter should be stripped from the original column
id1: expectedColumn,
col1: expect.any(Object),
})
);
});
it('should create a new no-input operation to use as reference (case new2)', () => {
// @ts-expect-error this function is not valid
operationDefinitionMap.testReference.requiredReferences = [

View file

@ -509,7 +509,18 @@ function applyReferenceTransition({
if (!hasExactMatch && isColumnValidAsReference({ validation, column: previousColumn })) {
hasExactMatch = true;
const newLayer = { ...layer, columns: { ...layer.columns, [newId]: { ...previousColumn } } };
const newLayer = {
...layer,
columns: {
...layer.columns,
[newId]: {
...previousColumn,
// drop the filter for the referenced column because the wrapping operation
// is filterable as well and will handle it one level higher.
filter: operationDefinition.filterable ? undefined : previousColumn.filter,
},
},
};
layer = {
...layer,
columnOrder: getColumnOrder(newLayer),

View file

@ -32,6 +32,7 @@ export const createMockedReferenceOperation = () => {
references: args.referenceIds,
};
}),
filterable: true,
isTransferable: jest.fn(),
toExpression: jest.fn().mockReturnValue([]),
getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }),

View file

@ -0,0 +1,66 @@
/*
* 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, { useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import { IndexPattern } from './types';
import { QueryStringInput, Query } from '../../../../../src/plugins/data/public';
export const QueryInput = ({
value,
onChange,
indexPattern,
isInvalid,
onSubmit,
disableAutoFocus,
}: {
value: Query;
onChange: (input: Query) => void;
indexPattern: IndexPattern;
isInvalid: boolean;
onSubmit: () => void;
disableAutoFocus?: boolean;
}) => {
const [inputValue, setInputValue] = useState(value);
useDebounce(() => onChange(inputValue), 256, [inputValue]);
const handleInputChange = (input: Query) => {
setInputValue(input);
};
return (
<QueryStringInput
dataTestSubj="indexPattern-filters-queryStringInput"
size="s"
disableAutoFocus={disableAutoFocus}
isInvalid={isInvalid}
bubbleSubmitEvent={false}
indexPatterns={[indexPattern]}
query={inputValue}
onChange={handleInputChange}
onSubmit={() => {
if (inputValue.query) {
onSubmit();
}
}}
placeholder={
inputValue.language === 'kuery'
? i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderKql', {
defaultMessage: '{example}',
values: { example: 'method : "GET" or status : "404"' },
})
: i18n.translate('xpack.lens.indexPattern.filters.queryPlaceholderLucene', {
defaultMessage: '{example}',
values: { example: 'method:GET OR status:404' },
})
}
languageSwitcherPopoverAnchorPosition="rightDown"
/>
);
};

View file

@ -7,6 +7,7 @@
import type { IUiSettingsClient } from 'kibana/public';
import {
AggFunctionsMapping,
EsaggsExpressionFunctionDefinition,
IndexPatternLoadExpressionFunctionDefinition,
} from '../../../../../src/plugins/data/public';
@ -29,11 +30,32 @@ function getExpressionForLayer(
indexPattern: IndexPattern,
uiSettings: IUiSettingsClient
): ExpressionAstExpression | null {
const { columns, columnOrder } = layer;
const { columnOrder } = layer;
if (columnOrder.length === 0 || !indexPattern) {
return null;
}
const columns = { ...layer.columns };
Object.keys(columns).forEach((columnId) => {
const column = columns[columnId];
const rootDef = operationDefinitionMap[column.operationType];
if (
'references' in column &&
rootDef.filterable &&
rootDef.input === 'fullReference' &&
column.filter
) {
// inherit filter to all referenced operations
column.references.forEach((referenceColumnId) => {
const referencedColumn = columns[referenceColumnId];
const referenceDef = operationDefinitionMap[column.operationType];
if (referenceDef.filterable) {
columns[referenceColumnId] = { ...referencedColumn, filter: column.filter };
}
});
}
});
const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const);
if (columnEntries.length) {
@ -44,10 +66,37 @@ function getExpressionForLayer(
if (def.input === 'fullReference') {
expressions.push(...def.toExpression(layer, colId, indexPattern));
} else {
const wrapInFilter = Boolean(def.filterable && col.filter);
let aggAst = def.toEsAggsFn(
col,
wrapInFilter ? `${colId}-metric` : colId,
indexPattern,
layer,
uiSettings
);
if (wrapInFilter) {
aggAst = buildExpressionFunction<AggFunctionsMapping['aggFilteredMetric']>(
'aggFilteredMetric',
{
id: colId,
enabled: true,
schema: 'metric',
customBucket: buildExpression([
buildExpressionFunction<AggFunctionsMapping['aggFilter']>('aggFilter', {
id: `${colId}-filter`,
enabled: true,
schema: 'bucket',
filter: JSON.stringify(col.filter),
}),
]),
customMetric: buildExpression({ type: 'expression', chain: [aggAst] }),
}
).toAst();
}
aggs.push(
buildExpression({
type: 'expression',
chain: [def.toEsAggsFn(col, colId, indexPattern, layer, uiSettings)],
chain: [aggAst],
})
);
}

View file

@ -12087,7 +12087,6 @@
"xpack.lens.indexPattern.terms.otherLabel": "その他",
"xpack.lens.indexPattern.terms.size": "値の数",
"xpack.lens.indexPattern.termsOf": "{name} のトップの値",
"xpack.lens.indexPattern.timeScale.advancedSettings": "高度なオプションを追加",
"xpack.lens.indexPattern.timeScale.enableTimeScale": "単位で正規化",
"xpack.lens.indexPattern.timeScale.label": "単位で正規化",
"xpack.lens.indexPattern.timeScale.tooltip": "基本の日付間隔に関係なく、常に指定された時間単位のレートとして表示されるように値を正規化します。",

View file

@ -12244,7 +12244,6 @@
"xpack.lens.indexPattern.terms.otherLabel": "其他",
"xpack.lens.indexPattern.terms.size": "值数目",
"xpack.lens.indexPattern.termsOf": "{name} 排名最前值",
"xpack.lens.indexPattern.timeScale.advancedSettings": "添加高级选项",
"xpack.lens.indexPattern.timeScale.enableTimeScale": "按单位标准化",
"xpack.lens.indexPattern.timeScale.label": "按单位标准化",
"xpack.lens.indexPattern.timeScale.tooltip": "将值标准化为始终显示为每指定时间单位速率,无论基础日期时间间隔是多少。",