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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -2312,7 +2312,7 @@
"description": [], "description": [],
"source": { "source": {
"path": "x-pack/plugins/observability/server/plugin.ts", "path": "x-pack/plugins/observability/server/plugin.ts",
"lineNumber": 22 "lineNumber": 23
}, },
"signature": [ "signature": [
"LazyScopedAnnotationsClientFactory" "LazyScopedAnnotationsClientFactory"
@ -2321,7 +2321,7 @@
], ],
"source": { "source": {
"path": "x-pack/plugins/observability/server/plugin.ts", "path": "x-pack/plugins/observability/server/plugin.ts",
"lineNumber": 21 "lineNumber": 22
}, },
"lifecycle": "setup", "lifecycle": "setup",
"initialIsOpen": true "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> | | | [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> | | | [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> | | | [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> | | | [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> | | | [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> | | | [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> | | | COUNT | <code>&quot;count&quot;</code> | |
| CUMULATIVE\_SUM | <code>&quot;cumulative_sum&quot;</code> | | | CUMULATIVE\_SUM | <code>&quot;cumulative_sum&quot;</code> | |
| DERIVATIVE | <code>&quot;derivative&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\_BOUNDS | <code>&quot;geo_bounds&quot;</code> | |
| GEO\_CENTROID | <code>&quot;geo_centroid&quot;</code> | | | GEO\_CENTROID | <code>&quot;geo_centroid&quot;</code> | |
| MAX | <code>&quot;max&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> | | | [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> | | | [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> | | | [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> | | | [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> | | | [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> | | | [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> | | | COUNT | <code>&quot;count&quot;</code> | |
| CUMULATIVE\_SUM | <code>&quot;cumulative_sum&quot;</code> | | | CUMULATIVE\_SUM | <code>&quot;cumulative_sum&quot;</code> | |
| DERIVATIVE | <code>&quot;derivative&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\_BOUNDS | <code>&quot;geo_bounds&quot;</code> | |
| GEO\_CENTROID | <code>&quot;geo_centroid&quot;</code> | | | GEO\_CENTROID | <code>&quot;geo_centroid&quot;</code> | |
| MAX | <code>&quot;max&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 output = this.write(aggConfigs) as any;
const configDsl = {} 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 the config requires subAggs, write them to the dsl as well
if (this.subAggs.length) { if (this.subAggs.length) {

View file

@ -21,7 +21,14 @@ import { TimeRange } from '../../../common';
function removeParentAggs(obj: any) { function removeParentAggs(obj: any) {
for (const prop in obj) { for (const prop in obj) {
if (prop === 'parentAggs') delete obj[prop]; 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 // advance the cursor and nest under the previous agg, or
// put it on the same level if the previous agg doesn't accept // put it on the same level if the previous agg doesn't accept
// sub aggs // 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; let subAggs: any;
parseParentAggs(dslLvlCursor, dsl); parseParentAggs(dslLvlCursor, dsl);
@ -206,6 +215,11 @@ export class AggConfigs {
subAggs = dsl.aggs || (dsl.aggs = {}); subAggs = dsl.aggs || (dsl.aggs = {});
} }
if (subAggs) {
_.each(subAggs, (agg) => {
parseParentAggs(subAggs, agg);
});
}
if (subAggs && nestedMetrics) { if (subAggs && nestedMetrics) {
nestedMetrics.forEach((agg: any) => { nestedMetrics.forEach((agg: any) => {
subAggs[agg.config.id] = agg.dsl; subAggs[agg.config.id] = agg.dsl;

View file

@ -32,6 +32,7 @@ export interface AggTypeConfig<
makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string); makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string);
ordered?: any; ordered?: any;
hasNoDsl?: boolean; hasNoDsl?: boolean;
hasNoDslParams?: boolean;
params?: Array<Partial<TParam>>; params?: Array<Partial<TParam>>;
valueType?: DatatableColumnType; valueType?: DatatableColumnType;
getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void);
@ -129,6 +130,12 @@ export class AggType<
* @type {Boolean} * @type {Boolean}
*/ */
hasNoDsl: 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 * The method to create a filter representation of the bucket
* @param {object} aggConfig The instance of the aggConfig * @param {object} aggConfig The instance of the aggConfig
@ -232,6 +239,7 @@ export class AggType<
this.makeLabel = config.makeLabel || constant(this.name); this.makeLabel = config.makeLabel || constant(this.name);
this.ordered = config.ordered; this.ordered = config.ordered;
this.hasNoDsl = !!config.hasNoDsl; this.hasNoDsl = !!config.hasNoDsl;
this.hasNoDslParams = !!config.hasNoDslParams;
if (config.createFilter) { if (config.createFilter) {
this.createFilter = 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.SUM_BUCKET, fn: metrics.getBucketSumMetricAgg },
{ name: METRIC_TYPES.MIN_BUCKET, fn: metrics.getBucketMinMetricAgg }, { name: METRIC_TYPES.MIN_BUCKET, fn: metrics.getBucketMinMetricAgg },
{ name: METRIC_TYPES.MAX_BUCKET, fn: metrics.getBucketMaxMetricAgg }, { 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_BOUNDS, fn: metrics.getGeoBoundsMetricAgg },
{ name: METRIC_TYPES.GEO_CENTROID, fn: metrics.getGeoCentroidMetricAgg }, { name: METRIC_TYPES.GEO_CENTROID, fn: metrics.getGeoCentroidMetricAgg },
], ],
@ -80,6 +81,7 @@ export const getAggTypesFunctions = () => [
metrics.aggBucketMax, metrics.aggBucketMax,
metrics.aggBucketMin, metrics.aggBucketMin,
metrics.aggBucketSum, metrics.aggBucketSum,
metrics.aggFilteredMetric,
metrics.aggCardinality, metrics.aggCardinality,
metrics.aggCount, metrics.aggCount,
metrics.aggCumulativeSum, metrics.aggCumulativeSum,

View file

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

View file

@ -6,12 +6,15 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { cloneDeep } from 'lodash';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { BucketAggType } from './bucket_agg_type'; import { BucketAggType } from './bucket_agg_type';
import { BUCKET_TYPES } from './bucket_agg_types'; import { BUCKET_TYPES } from './bucket_agg_types';
import { GeoBoundingBox } from './lib/geo_point'; import { GeoBoundingBox } from './lib/geo_point';
import { aggFilterFnName } from './filter_fn'; import { aggFilterFnName } from './filter_fn';
import { BaseAggParams } from '../types'; import { BaseAggParams } from '../types';
import { Query } from '../../../types';
import { buildEsQuery, getEsQueryConfig } from '../../../es_query';
const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', { const filterTitle = i18n.translate('data.search.aggs.buckets.filterTitle', {
defaultMessage: 'Filter', defaultMessage: 'Filter',
@ -21,7 +24,7 @@ export interface AggParamsFilter extends BaseAggParams {
geo_bounding_box?: GeoBoundingBox; geo_bounding_box?: GeoBoundingBox;
} }
export const getFilterBucketAgg = () => export const getFilterBucketAgg = ({ getConfig }: { getConfig: <T = any>(key: string) => any }) =>
new BucketAggType({ new BucketAggType({
name: BUCKET_TYPES.FILTER, name: BUCKET_TYPES.FILTER,
expressionName: aggFilterFnName, expressionName: aggFilterFnName,
@ -31,5 +34,27 @@ export const getFilterBucketAgg = () =>
{ {
name: 'geo_bounding_box', 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, "id": undefined,
"params": Object { "params": Object {
"customLabel": undefined, "customLabel": undefined,
"filter": undefined,
"geo_bounding_box": undefined, "geo_bounding_box": undefined,
"json": undefined, "json": undefined,
}, },
@ -46,6 +47,7 @@ describe('agg_expression_functions', () => {
"id": undefined, "id": undefined,
"params": Object { "params": Object {
"customLabel": undefined, "customLabel": undefined,
"filter": undefined,
"geo_bounding_box": Object { "geo_bounding_box": Object {
"wkt": "BBOX (-74.1, -71.12, 40.73, 40.01)", "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', () => { test('correctly parses json string argument', () => {
const actual = fn({ const actual = fn({
json: '{ "foo": true }', json: '{ "foo": true }',

View file

@ -17,7 +17,7 @@ export const aggFilterFnName = 'aggFilter';
type Input = any; type Input = any;
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.FILTER>; 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 Output = AggExpressionType;
type FunctionDefinition = ExpressionFunctionDefinition< type FunctionDefinition = ExpressionFunctionDefinition<
@ -59,6 +59,13 @@ export const aggFilter = (): FunctionDefinition => ({
defaultMessage: 'Filter results based on a point location within a bounding box', 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: { json: {
types: ['string'], types: ['string'],
help: i18n.translate('data.search.aggs.buckets.filter.json.help', { help: i18n.translate('data.search.aggs.buckets.filter.json.help', {
@ -75,6 +82,13 @@ export const aggFilter = (): FunctionDefinition => ({
fn: (input, args) => { fn: (input, args) => {
const { id, enabled, schema, ...rest } = 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 { return {
type: 'agg_type', type: 'agg_type',
value: { value: {
@ -84,7 +98,8 @@ export const aggFilter = (): FunctionDefinition => ({
type: BUCKET_TYPES.FILTER, type: BUCKET_TYPES.FILTER,
params: { params: {
...rest, ...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_min';
export * from './bucket_sum_fn'; export * from './bucket_sum_fn';
export * from './bucket_sum'; export * from './bucket_sum';
export * from './filtered_metric_fn';
export * from './filtered_metric';
export * from './cardinality_fn'; export * from './cardinality_fn';
export * from './cardinality'; export * from './cardinality';
export * from './count'; export * from './count';

View file

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

View file

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

View file

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

View file

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

View file

@ -329,6 +329,10 @@ export interface AggFunctionsMapping {
// //
// (undocumented) // (undocumented)
aggFilter: ReturnType<typeof aggFilter>; 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 // Warning: (ae-forgotten-export) The symbol "aggFilters" needs to be exported by the entry point index.d.ts
// //
// (undocumented) // (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) // 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) // @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 "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 // 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) // 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) // @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) // 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) // 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) // @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) // 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) // (undocumented)
DERIVATIVE = "derivative", DERIVATIVE = "derivative",
// (undocumented) // (undocumented)
FILTERED_METRIC = "filtered_metric",
// (undocumented)
GEO_BOUNDS = "geo_bounds", GEO_BOUNDS = "geo_bounds",
// (undocumented) // (undocumented)
GEO_CENTROID = "geo_centroid", 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: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: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/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/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 "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 // 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); service.setup(setupDeps);
const start = service.start(startDeps); const start = service.start(startDeps);
expect(start.types.getAll().buckets.length).toBe(11); 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', () => { test('registers custom agg types', () => {
@ -71,7 +71,7 @@ describe('AggsService - public', () => {
const start = service.start(startDeps); const start = service.start(startDeps);
expect(start.types.getAll().buckets.length).toBe(12); expect(start.types.getAll().buckets.length).toBe(12);
expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true); 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); expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true);
}); });
}); });

View file

@ -134,6 +134,10 @@ export interface AggFunctionsMapping {
// //
// (undocumented) // (undocumented)
aggFilter: ReturnType<typeof aggFilter>; 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 // Warning: (ae-forgotten-export) The symbol "aggFilters" needs to be exported by the entry point index.d.ts
// //
// (undocumented) // (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) // 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) // @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) // 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) // 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) // @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) // 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) // (undocumented)
DERIVATIVE = "derivative", DERIVATIVE = "derivative",
// (undocumented) // (undocumented)
FILTERED_METRIC = "filtered_metric",
// (undocumented)
GEO_BOUNDS = "geo_bounds", GEO_BOUNDS = "geo_bounds",
// (undocumented) // (undocumented)
GEO_CENTROID = "geo_centroid", GEO_CENTROID = "geo_centroid",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -132,7 +132,7 @@ export const getLineVisTypeDefinition = (
name: 'metric', name: 'metric',
title: i18n.translate('visTypeXy.line.metricTitle', { defaultMessage: 'Y-axis' }), title: i18n.translate('visTypeXy.line.metricTitle', { defaultMessage: 'Y-axis' }),
min: 1, min: 1,
aggFilter: ['!geo_centroid', '!geo_bounds'], aggFilter: ['!geo_centroid', '!geo_bounds', '!filtered_metric'],
defaults: [{ schema: 'metric', type: 'count' }], 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, resetIncomplete,
FieldBasedIndexPatternColumn, FieldBasedIndexPatternColumn,
canTransition, canTransition,
DEFAULT_TIME_SCALE,
} from '../operations'; } from '../operations';
import { mergeLayer } from '../state_helpers'; import { mergeLayer } from '../state_helpers';
import { FieldSelect } from './field_select'; import { FieldSelect } from './field_select';
@ -41,7 +42,9 @@ import { IndexPattern, IndexPatternLayer } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry'; import { trackUiEvent } from '../../lens_ui_telemetry';
import { FormatSelector } from './format_selector'; import { FormatSelector } from './format_selector';
import { ReferenceEditor } from './reference_editor'; 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(); const operationPanels = getOperationDisplay();
@ -156,6 +159,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
.filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type)); .filter((type) => fieldByOperation[type]?.size || operationWithoutField.has(type));
}, [fieldByOperation, operationWithoutField]); }, [fieldByOperation, operationWithoutField]);
const [filterByOpenInitially, setFilterByOpenInitally] = useState(false);
// Operations are compatible if they match inputs. They are always compatible in // 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. // the empty state. Field-based operations are not compatible with field-less operations.
const operationsWithCompatibility = [...possibleOperations].map((operationType) => { const operationsWithCompatibility = [...possibleOperations].map((operationType) => {
@ -458,11 +463,63 @@ export function DimensionEditor(props: DimensionEditorProps) {
)} )}
{!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ( {!currentFieldIsInvalid && !incompleteInfo && selectedColumn && (
<TimeScaling <AdvancedOptions
selectedColumn={selectedColumn} options={[
columnId={columnId} {
layer={state.layers[layerId]} title: i18n.translate('xpack.lens.indexPattern.timeScale.enableTimeScale', {
updateLayer={setStateWrapper} 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> </div>

View file

@ -6,7 +6,7 @@
*/ */
import { ReactWrapper, ShallowWrapper } from 'enzyme'; 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 { act } from 'react-dom/test-utils';
import { import {
EuiComboBox, EuiComboBox,
@ -15,6 +15,7 @@ import {
EuiRange, EuiRange,
EuiSelect, EuiSelect,
EuiButtonIcon, EuiButtonIcon,
EuiPopover,
} from '@elastic/eui'; } from '@elastic/eui';
import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public';
import { import {
@ -30,10 +31,14 @@ import { documentField } from '../document_field';
import { OperationMetadata } from '../../types'; import { OperationMetadata } from '../../types';
import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram';
import { getFieldByNameFactory } from '../pure_helpers'; import { getFieldByNameFactory } from '../pure_helpers';
import { TimeScaling } from './time_scaling';
import { DimensionEditor } from './dimension_editor'; import { DimensionEditor } from './dimension_editor';
import { AdvancedOptions } from './advanced_options';
import { Filtering } from './filtering';
jest.mock('../loader'); jest.mock('../loader');
jest.mock('../query_input', () => ({
QueryInput: () => null,
}));
jest.mock('../operations'); jest.mock('../operations');
jest.mock('lodash', () => { jest.mock('lodash', () => {
const original = jest.requireActual('lodash'); const original = jest.requireActual('lodash');
@ -1029,7 +1034,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
} }
it('should not show custom options if time scaling is not available', () => { it('should not show custom options if time scaling is not available', () => {
wrapper = mount( wrapper = shallow(
<IndexPatternDimensionEditorComponent <IndexPatternDimensionEditorComponent
{...getProps({ {...getProps({
operationType: 'avg', 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', () => { it('should show custom options if time scaling is available', () => {
wrapper = mount(<IndexPatternDimensionEditorComponent {...getProps({})} />); wrapper = shallow(<IndexPatternDimensionEditorComponent {...getProps({})} />);
expect( expect(
wrapper wrapper
.find(TimeScaling) .find(DimensionEditor)
.find('[data-test-subj="indexPattern-time-scaling-popover"]') .dive()
.exists() .find(AdvancedOptions)
).toBe(true); .dive()
.find('[data-test-subj="indexPattern-time-scaling-enable"]')
).toHaveLength(1);
}); });
it('should show current time scaling if set', () => { it('should show current time scaling if set', () => {
@ -1066,7 +1080,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
wrapper wrapper
.find(DimensionEditor) .find(DimensionEditor)
.dive() .dive()
.find(TimeScaling) .find(AdvancedOptions)
.dive() .dive()
.find('[data-test-subj="indexPattern-time-scaling-enable"]') .find('[data-test-subj="indexPattern-time-scaling-enable"]')
.prop('onClick')!({} as MouseEvent); .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', () => { it('should render invalid field if field reference is broken', () => {
wrapper = mount( wrapper = mount(
<IndexPatternDimensionEditorComponent <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 { EuiToolTip } from '@elastic/eui';
import { EuiIcon } from '@elastic/eui'; import { EuiIcon } from '@elastic/eui';
import { import { EuiFormRow, EuiSelect, EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui';
EuiLink,
EuiFormRow,
EuiSelect,
EuiFlexItem,
EuiFlexGroup,
EuiButtonIcon,
EuiText,
EuiPopover,
EuiButtonEmpty,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React, { useState } from 'react'; import React from 'react';
import { import {
adjustTimeScaleLabelSuffix, adjustTimeScaleLabelSuffix,
DEFAULT_TIME_SCALE,
IndexPatternColumn, IndexPatternColumn,
operationDefinitionMap, operationDefinitionMap,
} from '../operations'; } from '../operations';
@ -64,7 +52,6 @@ export function TimeScaling({
layer: IndexPatternLayer; layer: IndexPatternLayer;
updateLayer: (newLayer: IndexPatternLayer) => void; updateLayer: (newLayer: IndexPatternLayer) => void;
}) { }) {
const [popoverOpen, setPopoverOpen] = useState(false);
const hasDateHistogram = layer.columnOrder.some( const hasDateHistogram = layer.columnOrder.some(
(colId) => layer.columns[colId].operationType === 'date_histogram' (colId) => layer.columns[colId].operationType === 'date_histogram'
); );
@ -72,56 +59,12 @@ export function TimeScaling({
if ( if (
!selectedOperation.timeScalingMode || !selectedOperation.timeScalingMode ||
selectedOperation.timeScalingMode === 'disabled' || selectedOperation.timeScalingMode === 'disabled' ||
!hasDateHistogram !hasDateHistogram ||
!selectedColumn.timeScale
) { ) {
return null; 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 ( return (
<EuiFormRow <EuiFormRow
display="columnCompressed" 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 () => { it('should add time_scale and format function if time scale is set and supported', async () => {
const queryBaseState: IndexPatternBaseState = { const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1', currentIndexPatternId: '1',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,13 +8,12 @@
import './filter_popover.scss'; import './filter_popover.scss';
import React, { MouseEventHandler, useEffect, useState } from 'react'; import React, { MouseEventHandler, useEffect, useState } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { EuiPopover, EuiSpacer } from '@elastic/eui'; import { EuiPopover, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilterValue, defaultLabel, isQueryValid } from '.'; import { FilterValue, defaultLabel, isQueryValid } from '.';
import { IndexPattern } from '../../../types'; 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 { LabelInput } from '../shared_components';
import { QueryInput } from '../../../query_input';
export const FilterPopover = ({ export const FilterPopover = ({
filter, filter,
@ -94,54 +93,3 @@ export const FilterPopover = ({
</EuiPopover> </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. * If set to optional, time scaling won't be enabled by default and can be removed.
*/ */
timeScalingMode?: TimeScalingMode; timeScalingMode?: TimeScalingMode;
filterable?: boolean;
getHelpMessage?: (props: HelpProps<C>) => React.ReactNode; getHelpMessage?: (props: HelpProps<C>) => React.ReactNode;
} }

View file

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

View file

@ -99,6 +99,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
isBucketed: false, isBucketed: false,
scale: 'ratio', scale: 'ratio',
timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined, timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined,
filter: previousColumn?.filter,
params: getFormatFromPreviousColumn(previousColumn), params: getFormatFromPreviousColumn(previousColumn),
} as T), } as T),
onFieldChange: (oldColumn, field) => { onFieldChange: (oldColumn, field) => {
@ -118,6 +119,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
}, },
getErrorMessage: (layer, columnId, indexPattern) => getErrorMessage: (layer, columnId, indexPattern) =>
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern), getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
filterable: true,
} as OperationDefinition<T, 'field'>; } 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)', () => { it('should create a new no-input operation to use as reference (case new2)', () => {
// @ts-expect-error this function is not valid // @ts-expect-error this function is not valid
operationDefinitionMap.testReference.requiredReferences = [ operationDefinitionMap.testReference.requiredReferences = [

View file

@ -509,7 +509,18 @@ function applyReferenceTransition({
if (!hasExactMatch && isColumnValidAsReference({ validation, column: previousColumn })) { if (!hasExactMatch && isColumnValidAsReference({ validation, column: previousColumn })) {
hasExactMatch = true; 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 = {
...layer, ...layer,
columnOrder: getColumnOrder(newLayer), columnOrder: getColumnOrder(newLayer),

View file

@ -32,6 +32,7 @@ export const createMockedReferenceOperation = () => {
references: args.referenceIds, references: args.referenceIds,
}; };
}), }),
filterable: true,
isTransferable: jest.fn(), isTransferable: jest.fn(),
toExpression: jest.fn().mockReturnValue([]), toExpression: jest.fn().mockReturnValue([]),
getPossibleOperation: jest.fn().mockReturnValue({ dataType: 'number', isBucketed: false }), 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 type { IUiSettingsClient } from 'kibana/public';
import { import {
AggFunctionsMapping,
EsaggsExpressionFunctionDefinition, EsaggsExpressionFunctionDefinition,
IndexPatternLoadExpressionFunctionDefinition, IndexPatternLoadExpressionFunctionDefinition,
} from '../../../../../src/plugins/data/public'; } from '../../../../../src/plugins/data/public';
@ -29,11 +30,32 @@ function getExpressionForLayer(
indexPattern: IndexPattern, indexPattern: IndexPattern,
uiSettings: IUiSettingsClient uiSettings: IUiSettingsClient
): ExpressionAstExpression | null { ): ExpressionAstExpression | null {
const { columns, columnOrder } = layer; const { columnOrder } = layer;
if (columnOrder.length === 0 || !indexPattern) { if (columnOrder.length === 0 || !indexPattern) {
return null; 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); const columnEntries = columnOrder.map((colId) => [colId, columns[colId]] as const);
if (columnEntries.length) { if (columnEntries.length) {
@ -44,10 +66,37 @@ function getExpressionForLayer(
if (def.input === 'fullReference') { if (def.input === 'fullReference') {
expressions.push(...def.toExpression(layer, colId, indexPattern)); expressions.push(...def.toExpression(layer, colId, indexPattern));
} else { } 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( aggs.push(
buildExpression({ buildExpression({
type: 'expression', 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.otherLabel": "その他",
"xpack.lens.indexPattern.terms.size": "値の数", "xpack.lens.indexPattern.terms.size": "値の数",
"xpack.lens.indexPattern.termsOf": "{name} のトップの値", "xpack.lens.indexPattern.termsOf": "{name} のトップの値",
"xpack.lens.indexPattern.timeScale.advancedSettings": "高度なオプションを追加",
"xpack.lens.indexPattern.timeScale.enableTimeScale": "単位で正規化", "xpack.lens.indexPattern.timeScale.enableTimeScale": "単位で正規化",
"xpack.lens.indexPattern.timeScale.label": "単位で正規化", "xpack.lens.indexPattern.timeScale.label": "単位で正規化",
"xpack.lens.indexPattern.timeScale.tooltip": "基本の日付間隔に関係なく、常に指定された時間単位のレートとして表示されるように値を正規化します。", "xpack.lens.indexPattern.timeScale.tooltip": "基本の日付間隔に関係なく、常に指定された時間単位のレートとして表示されるように値を正規化します。",

View file

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