mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] Implement rare terms (#121500)
This commit is contained in:
parent
e9f45f63f2
commit
38de5842a9
41 changed files with 778 additions and 132 deletions
|
@ -147,6 +147,7 @@ readonly links: {
|
|||
readonly significant_terms: string;
|
||||
readonly terms: string;
|
||||
readonly terms_doc_count_error: string;
|
||||
readonly rare_terms: string;
|
||||
readonly avg: string;
|
||||
readonly avg_bucket: string;
|
||||
readonly max_bucket: string;
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -179,6 +179,7 @@ export class DocLinksService {
|
|||
significant_terms: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-significantterms-aggregation.html`,
|
||||
terms: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-terms-aggregation.html`,
|
||||
terms_doc_count_error: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-terms-aggregation.html#_per_bucket_document_count_error`,
|
||||
rare_terms: `${ELASTICSEARCH_DOCS}search-aggregations-bucket-rare-terms-aggregation.html`,
|
||||
avg: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-avg-aggregation.html`,
|
||||
avg_bucket: `${ELASTICSEARCH_DOCS}search-aggregations-pipeline-avg-bucket-aggregation.html`,
|
||||
max_bucket: `${ELASTICSEARCH_DOCS}search-aggregations-pipeline-max-bucket-aggregation.html`,
|
||||
|
@ -746,6 +747,7 @@ export interface DocLinksStart {
|
|||
readonly significant_terms: string;
|
||||
readonly terms: string;
|
||||
readonly terms_doc_count_error: string;
|
||||
readonly rare_terms: string;
|
||||
readonly avg: string;
|
||||
readonly avg_bucket: string;
|
||||
readonly max_bucket: string;
|
||||
|
|
|
@ -630,6 +630,7 @@ export interface DocLinksStart {
|
|||
readonly significant_terms: string;
|
||||
readonly terms: string;
|
||||
readonly terms_doc_count_error: string;
|
||||
readonly rare_terms: string;
|
||||
readonly avg: string;
|
||||
readonly avg_bucket: string;
|
||||
readonly max_bucket: string;
|
||||
|
|
|
@ -56,6 +56,7 @@ export const getAggTypes = () => ({
|
|||
{ name: BUCKET_TYPES.IP_RANGE, fn: buckets.getIpRangeBucketAgg },
|
||||
{ name: BUCKET_TYPES.TERMS, fn: buckets.getTermsBucketAgg },
|
||||
{ name: BUCKET_TYPES.MULTI_TERMS, fn: buckets.getMultiTermsBucketAgg },
|
||||
{ name: BUCKET_TYPES.RARE_TERMS, fn: buckets.getRareTermsBucketAgg },
|
||||
{ name: BUCKET_TYPES.FILTER, fn: buckets.getFilterBucketAgg },
|
||||
{ name: BUCKET_TYPES.FILTERS, fn: buckets.getFiltersBucketAgg },
|
||||
{ name: BUCKET_TYPES.SIGNIFICANT_TERMS, fn: buckets.getSignificantTermsBucketAgg },
|
||||
|
@ -82,6 +83,7 @@ export const getAggTypesFunctions = () => [
|
|||
buckets.aggDateHistogram,
|
||||
buckets.aggTerms,
|
||||
buckets.aggMultiTerms,
|
||||
buckets.aggRareTerms,
|
||||
buckets.aggSampler,
|
||||
buckets.aggDiversifiedSampler,
|
||||
metrics.aggAvg,
|
||||
|
|
|
@ -68,6 +68,7 @@ describe('Aggs service', () => {
|
|||
"ip_range",
|
||||
"terms",
|
||||
"multi_terms",
|
||||
"rare_terms",
|
||||
"filter",
|
||||
"filters",
|
||||
"significant_terms",
|
||||
|
@ -120,6 +121,7 @@ describe('Aggs service', () => {
|
|||
"ip_range",
|
||||
"terms",
|
||||
"multi_terms",
|
||||
"rare_terms",
|
||||
"filter",
|
||||
"filters",
|
||||
"significant_terms",
|
||||
|
|
|
@ -15,6 +15,7 @@ export enum BUCKET_TYPES {
|
|||
RANGE = 'range',
|
||||
TERMS = 'terms',
|
||||
MULTI_TERMS = 'multi_terms',
|
||||
RARE_TERMS = 'rare_terms',
|
||||
SIGNIFICANT_TERMS = 'significant_terms',
|
||||
SIGNIFICANT_TEXT = 'significant_text',
|
||||
GEOHASH_GRID = 'geohash_grid',
|
||||
|
|
|
@ -40,6 +40,8 @@ export * from './terms_fn';
|
|||
export * from './terms';
|
||||
export * from './multi_terms_fn';
|
||||
export * from './multi_terms';
|
||||
export * from './rare_terms_fn';
|
||||
export * from './rare_terms';
|
||||
export * from './sampler_fn';
|
||||
export * from './sampler';
|
||||
export * from './diversified_sampler_fn';
|
||||
|
|
|
@ -127,7 +127,7 @@ describe('Multi Terms Agg', () => {
|
|||
5,
|
||||
],
|
||||
},
|
||||
"function": "aggTerms",
|
||||
"function": "aggMultiTerms",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
|
|
|
@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { BucketAggType } from './bucket_agg_type';
|
||||
import { BUCKET_TYPES } from './bucket_agg_types';
|
||||
import { createFilterMultiTerms } from './create_filter/multi_terms';
|
||||
import { aggTermsFnName } from './terms_fn';
|
||||
import { aggMultiTermsFnName } from './multi_terms_fn';
|
||||
import { AggConfigSerialized, BaseAggParams } from '../types';
|
||||
|
||||
import { MultiFieldKey } from './multi_field_key';
|
||||
|
@ -41,7 +41,7 @@ export const getMultiTermsBucketAgg = () => {
|
|||
const keyCaches = new WeakMap();
|
||||
return new BucketAggType({
|
||||
name: BUCKET_TYPES.MULTI_TERMS,
|
||||
expressionName: aggTermsFnName,
|
||||
expressionName: aggMultiTermsFnName,
|
||||
title: termsTitle,
|
||||
makeLabel(agg) {
|
||||
const params = agg.params;
|
||||
|
|
103
src/plugins/data/common/search/aggs/buckets/rare_terms.test.ts
Normal file
103
src/plugins/data/common/search/aggs/buckets/rare_terms.test.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 } from '../agg_configs';
|
||||
import { mockAggTypesRegistry } from '../test_helpers';
|
||||
import { BUCKET_TYPES } from './bucket_agg_types';
|
||||
import type { IndexPatternField } from '../../..';
|
||||
import { IndexPattern } from '../../..';
|
||||
|
||||
describe('rare terms Agg', () => {
|
||||
const getAggConfigs = (params: Record<string, any> = {}) => {
|
||||
const indexPattern = {
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields: [
|
||||
{
|
||||
name: 'field',
|
||||
type: 'string',
|
||||
esTypes: ['string'],
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'string_field',
|
||||
type: 'string',
|
||||
esTypes: ['string'],
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'empty_number_field',
|
||||
type: 'number',
|
||||
esTypes: ['number'],
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'number_field',
|
||||
type: 'number',
|
||||
esTypes: ['number'],
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
} as IndexPattern;
|
||||
|
||||
indexPattern.fields.getByName = (name) => ({ name } as unknown as IndexPatternField);
|
||||
indexPattern.fields.filter = () => indexPattern.fields;
|
||||
|
||||
return new AggConfigs(
|
||||
indexPattern,
|
||||
[
|
||||
{
|
||||
id: 'test',
|
||||
params,
|
||||
type: BUCKET_TYPES.RARE_TERMS,
|
||||
},
|
||||
],
|
||||
{ typesRegistry: mockAggTypesRegistry() }
|
||||
);
|
||||
};
|
||||
|
||||
test('produces the expected expression ast', () => {
|
||||
const aggConfigs = getAggConfigs({
|
||||
field: 'field',
|
||||
max_doc_count: 5,
|
||||
});
|
||||
expect(aggConfigs.aggs[0].toExpressionAst()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"enabled": Array [
|
||||
true,
|
||||
],
|
||||
"field": Array [
|
||||
"field",
|
||||
],
|
||||
"id": Array [
|
||||
"test",
|
||||
],
|
||||
"max_doc_count": Array [
|
||||
5,
|
||||
],
|
||||
},
|
||||
"function": "aggRareTerms",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
60
src/plugins/data/common/search/aggs/buckets/rare_terms.ts
Normal file
60
src/plugins/data/common/search/aggs/buckets/rare_terms.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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 { BucketAggType } from './bucket_agg_type';
|
||||
import { BUCKET_TYPES } from './bucket_agg_types';
|
||||
import { createFilterTerms } from './create_filter/terms';
|
||||
import { aggRareTermsFnName } from './rare_terms_fn';
|
||||
import { BaseAggParams } from '../types';
|
||||
|
||||
import { KBN_FIELD_TYPES } from '../../../../common';
|
||||
|
||||
const termsTitle = i18n.translate('data.search.aggs.buckets.rareTermsTitle', {
|
||||
defaultMessage: 'Rare terms',
|
||||
});
|
||||
|
||||
export interface AggParamsRareTerms extends BaseAggParams {
|
||||
field: string;
|
||||
max_doc_count?: number;
|
||||
}
|
||||
|
||||
export const getRareTermsBucketAgg = () => {
|
||||
return new BucketAggType({
|
||||
name: BUCKET_TYPES.RARE_TERMS,
|
||||
expressionName: aggRareTermsFnName,
|
||||
title: termsTitle,
|
||||
makeLabel(agg) {
|
||||
return i18n.translate('data.search.aggs.rareTerms.aggTypesLabel', {
|
||||
defaultMessage: 'Rare terms of {fieldName}',
|
||||
values: {
|
||||
fieldName: agg.getFieldDisplayName(),
|
||||
},
|
||||
});
|
||||
},
|
||||
createFilter: createFilterTerms,
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
type: 'field',
|
||||
filterFieldTypes: [
|
||||
KBN_FIELD_TYPES.NUMBER,
|
||||
KBN_FIELD_TYPES.BOOLEAN,
|
||||
KBN_FIELD_TYPES.DATE,
|
||||
KBN_FIELD_TYPES.IP,
|
||||
KBN_FIELD_TYPES.STRING,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'max_doc_count',
|
||||
default: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
88
src/plugins/data/common/search/aggs/buckets/rare_terms_fn.ts
Normal file
88
src/plugins/data/common/search/aggs/buckets/rare_terms_fn.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
|
||||
import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../';
|
||||
|
||||
export const aggRareTermsFnName = 'aggRareTerms';
|
||||
|
||||
type Input = any;
|
||||
type AggArgs = AggExpressionFunctionArgs<typeof BUCKET_TYPES.RARE_TERMS>;
|
||||
|
||||
type Output = AggExpressionType;
|
||||
type FunctionDefinition = ExpressionFunctionDefinition<
|
||||
typeof aggRareTermsFnName,
|
||||
Input,
|
||||
AggArgs,
|
||||
Output
|
||||
>;
|
||||
|
||||
export const aggRareTerms = (): FunctionDefinition => ({
|
||||
name: aggRareTermsFnName,
|
||||
help: i18n.translate('data.search.aggs.function.buckets.rareTerms.help', {
|
||||
defaultMessage: 'Generates a serialized agg config for a Rare-Terms agg',
|
||||
}),
|
||||
type: 'agg_type',
|
||||
args: {
|
||||
id: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('data.search.aggs.buckets.rareTerms.id.help', {
|
||||
defaultMessage: 'ID for this aggregation',
|
||||
}),
|
||||
},
|
||||
enabled: {
|
||||
types: ['boolean'],
|
||||
default: true,
|
||||
help: i18n.translate('data.search.aggs.buckets.rareTerms.enabled.help', {
|
||||
defaultMessage: 'Specifies whether this aggregation should be enabled',
|
||||
}),
|
||||
},
|
||||
schema: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('data.search.aggs.buckets.rareTerms.schema.help', {
|
||||
defaultMessage: 'Schema to use for this aggregation',
|
||||
}),
|
||||
},
|
||||
field: {
|
||||
types: ['string'],
|
||||
required: true,
|
||||
help: i18n.translate('data.search.aggs.buckets.rareTerms.fields.help', {
|
||||
defaultMessage: 'Field to use for this aggregation',
|
||||
}),
|
||||
},
|
||||
max_doc_count: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('data.search.aggs.buckets.rareTerms.maxDocCount.help', {
|
||||
defaultMessage: 'Maximum number of times a term is allowed to occur to qualify as rare',
|
||||
}),
|
||||
},
|
||||
json: {
|
||||
types: ['string'],
|
||||
help: i18n.translate('data.search.aggs.buckets.multiTerms.json.help', {
|
||||
defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch',
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
const { id, enabled, schema, ...rest } = args;
|
||||
|
||||
return {
|
||||
type: 'agg_type',
|
||||
value: {
|
||||
id,
|
||||
enabled,
|
||||
schema,
|
||||
type: BUCKET_TYPES.RARE_TERMS,
|
||||
params: {
|
||||
...rest,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -68,6 +68,7 @@ import {
|
|||
AggParamsSum,
|
||||
AggParamsTerms,
|
||||
AggParamsMultiTerms,
|
||||
AggParamsRareTerms,
|
||||
AggParamsTopHit,
|
||||
aggPercentileRanks,
|
||||
aggPercentiles,
|
||||
|
@ -78,6 +79,7 @@ import {
|
|||
aggSum,
|
||||
aggTerms,
|
||||
aggMultiTerms,
|
||||
aggRareTerms,
|
||||
aggTopHit,
|
||||
AggTypesRegistry,
|
||||
AggTypesRegistrySetup,
|
||||
|
@ -166,6 +168,7 @@ export interface AggParamsMapping {
|
|||
[BUCKET_TYPES.DATE_HISTOGRAM]: AggParamsDateHistogram;
|
||||
[BUCKET_TYPES.TERMS]: AggParamsTerms;
|
||||
[BUCKET_TYPES.MULTI_TERMS]: AggParamsMultiTerms;
|
||||
[BUCKET_TYPES.RARE_TERMS]: AggParamsRareTerms;
|
||||
[BUCKET_TYPES.SAMPLER]: AggParamsSampler;
|
||||
[BUCKET_TYPES.DIVERSIFIED_SAMPLER]: AggParamsDiversifiedSampler;
|
||||
[METRIC_TYPES.AVG]: AggParamsAvg;
|
||||
|
@ -209,6 +212,7 @@ export interface AggFunctionsMapping {
|
|||
aggDateHistogram: ReturnType<typeof aggDateHistogram>;
|
||||
aggTerms: ReturnType<typeof aggTerms>;
|
||||
aggMultiTerms: ReturnType<typeof aggMultiTerms>;
|
||||
aggRareTerms: ReturnType<typeof aggRareTerms>;
|
||||
aggAvg: ReturnType<typeof aggAvg>;
|
||||
aggBucketAvg: ReturnType<typeof aggBucketAvg>;
|
||||
aggBucketMax: ReturnType<typeof aggBucketMax>;
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('AggsService - public', () => {
|
|||
test('registers default agg types', () => {
|
||||
service.setup(setupDeps);
|
||||
const start = service.start(startDeps);
|
||||
expect(start.types.getAll().buckets.length).toBe(15);
|
||||
expect(start.types.getAll().buckets.length).toBe(16);
|
||||
expect(start.types.getAll().metrics.length).toBe(23);
|
||||
});
|
||||
|
||||
|
@ -69,7 +69,7 @@ describe('AggsService - public', () => {
|
|||
);
|
||||
|
||||
const start = service.start(startDeps);
|
||||
expect(start.types.getAll().buckets.length).toBe(16);
|
||||
expect(start.types.getAll().buckets.length).toBe(17);
|
||||
expect(start.types.getAll().buckets.some(({ name }) => name === 'foo')).toBe(true);
|
||||
expect(start.types.getAll().metrics.length).toBe(24);
|
||||
expect(start.types.getAll().metrics.some(({ name }) => name === 'bar')).toBe(true);
|
||||
|
|
|
@ -104,6 +104,7 @@ export const getHeatmapVisTypeDefinition = ({
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -120,6 +121,7 @@ export const getHeatmapVisTypeDefinition = ({
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -143,6 +145,7 @@ export const getHeatmapVisTypeDefinition = ({
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -92,6 +92,7 @@ export const createMetricVisTypeDefinition = (): VisTypeDefinition<VisParams> =>
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -93,6 +93,7 @@ export const samplePieVis = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -112,6 +113,7 @@ export const samplePieVis = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -93,6 +93,7 @@ export const getPieVisTypeDefinition = ({
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -112,6 +113,7 @@ export const getPieVisTypeDefinition = ({
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -68,6 +68,7 @@ export const tableVisTypeDefinition: VisTypeDefinition<TableVisParams> = {
|
|||
'!diversified_sampler',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
'!rare_terms',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -84,6 +85,7 @@ export const tableVisTypeDefinition: VisTypeDefinition<TableVisParams> = {
|
|||
'!diversified_sampler',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
'!rare_terms',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -138,6 +138,7 @@ export const gaugeVisTypeDefinition: VisTypeDefinition<GaugeVisParams> = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -102,6 +102,7 @@ export const goalVisTypeDefinition: VisTypeDefinition<GaugeVisParams> = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -155,6 +155,7 @@ export const sampleAreaVis = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -173,6 +174,7 @@ export const sampleAreaVis = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -191,6 +193,7 @@ export const sampleAreaVis = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -163,6 +163,7 @@ export const areaVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -181,6 +182,7 @@ export const areaVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -199,6 +201,7 @@ export const areaVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -166,6 +166,7 @@ export const histogramVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -184,6 +185,7 @@ export const histogramVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -202,6 +204,7 @@ export const histogramVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -165,6 +165,7 @@ export const horizontalBarVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -183,6 +184,7 @@ export const horizontalBarVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -201,6 +203,7 @@ export const horizontalBarVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -157,6 +157,7 @@ export const lineVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -175,6 +176,7 @@ export const lineVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
@ -193,6 +195,7 @@ export const lineVisTypeDefinition = {
|
|||
'!filter',
|
||||
'!sampler',
|
||||
'!diversified_sampler',
|
||||
'!rare_terms',
|
||||
'!multi_terms',
|
||||
'!significant_text',
|
||||
],
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { DatatableRow } from 'src/plugins/expressions';
|
||||
import { ExpectExpression, expectExpressionProvider } from './helpers';
|
||||
import { FtrProviderContext } from '../../../functional/ftr_provider_context';
|
||||
|
||||
export default function ({
|
||||
getService,
|
||||
updateBaselines,
|
||||
}: FtrProviderContext & { updateBaselines: boolean }) {
|
||||
let expectExpression: ExpectExpression;
|
||||
|
||||
describe('esaggs_rareterms', () => {
|
||||
before(() => {
|
||||
expectExpression = expectExpressionProvider({ getService, updateBaselines });
|
||||
});
|
||||
|
||||
const timeRange = {
|
||||
from: '2015-09-21T00:00:00Z',
|
||||
to: '2015-09-22T00:00:00Z',
|
||||
};
|
||||
|
||||
describe('aggRareTerms', () => {
|
||||
it('can execute aggRareTerms', async () => {
|
||||
const expression = `
|
||||
kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'}
|
||||
| esaggs index={indexPatternLoad id='logstash-*'}
|
||||
aggs={aggRareTerms id="1" enabled=true schema="bucket" field="geo.srcdest" max_doc_count=1}
|
||||
aggs={aggCount id="2" enabled=true schema="metric"}
|
||||
`;
|
||||
const result = await expectExpression('rareterms', expression).getResponse();
|
||||
|
||||
expect(result.rows.length).to.be(1149);
|
||||
result.rows.forEach((row: DatatableRow) => {
|
||||
expect(row['col-1-2']).to.be(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -46,5 +46,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
|
|||
loadTestFile(require.resolve('./esaggs_multiterms'));
|
||||
loadTestFile(require.resolve('./esaggs_sampler'));
|
||||
loadTestFile(require.resolve('./esaggs_significanttext'));
|
||||
loadTestFile(require.resolve('./esaggs_rareterms'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
updateVisualizationState,
|
||||
DatasourceStates,
|
||||
VisualizationState,
|
||||
updateDatasourceState,
|
||||
} from '../../../state_management';
|
||||
import { WorkspaceTitle } from './title';
|
||||
|
||||
|
@ -60,6 +61,17 @@ export function WorkspacePanelWrapper({
|
|||
},
|
||||
[dispatchLens, activeVisualization]
|
||||
);
|
||||
const setDatasourceState = useCallback(
|
||||
(updater: unknown, datasourceId: string) => {
|
||||
dispatchLens(
|
||||
updateDatasourceState({
|
||||
updater,
|
||||
datasourceId,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatchLens]
|
||||
);
|
||||
const warningMessages: React.ReactNode[] = [];
|
||||
if (activeVisualization?.getWarningMessages) {
|
||||
warningMessages.push(
|
||||
|
@ -70,7 +82,9 @@ export function WorkspacePanelWrapper({
|
|||
const datasource = datasourceMap[datasourceId];
|
||||
if (!datasourceState.isLoading && datasource.getWarningMessages) {
|
||||
warningMessages.push(
|
||||
...(datasource.getWarningMessages(datasourceState.state, framePublicAPI) || [])
|
||||
...(datasource.getWarningMessages(datasourceState.state, framePublicAPI, (updater) =>
|
||||
setDatasourceState(updater, datasourceId)
|
||||
) || [])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -634,6 +634,20 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
},
|
||||
];
|
||||
|
||||
const defaultLabel = useMemo(
|
||||
() =>
|
||||
String(
|
||||
selectedColumn &&
|
||||
!selectedColumn.customLabel &&
|
||||
operationDefinitionMap[selectedColumn.operationType].getDefaultLabel(
|
||||
selectedColumn,
|
||||
state.indexPatterns[state.layers[layerId].indexPatternId],
|
||||
state.layers[layerId].columns
|
||||
)
|
||||
),
|
||||
[layerId, selectedColumn, state.indexPatterns, state.layers]
|
||||
);
|
||||
|
||||
return (
|
||||
<div id={columnId}>
|
||||
{hasTabs ? <DimensionEditorTabs tabs={tabs} /> : null}
|
||||
|
@ -750,6 +764,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--collapseNext">
|
||||
{!incompleteInfo && selectedColumn && temporaryState === 'none' && (
|
||||
<LabelInput
|
||||
// re-render the input from scratch to obtain new "initial value" if the underlying default label changes
|
||||
key={defaultLabel}
|
||||
value={selectedColumn.label}
|
||||
onChange={(value) => {
|
||||
updateLayer({
|
||||
|
|
|
@ -1449,7 +1449,7 @@ describe('IndexPattern Data Source', () => {
|
|||
});
|
||||
|
||||
it('should return mismatched time shifts', () => {
|
||||
const warnings = indexPatternDatasource.getWarningMessages!(state, framePublicAPI);
|
||||
const warnings = indexPatternDatasource.getWarningMessages!(state, framePublicAPI, () => {});
|
||||
|
||||
expect(warnings!.map((item) => (item as React.ReactElement).props.id)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
@ -1462,7 +1462,7 @@ describe('IndexPattern Data Source', () => {
|
|||
it('should show different types of warning messages', () => {
|
||||
framePublicAPI.activeData!.first.columns[0].meta.sourceParams!.hasPrecisionError = true;
|
||||
|
||||
const warnings = indexPatternDatasource.getWarningMessages!(state, framePublicAPI);
|
||||
const warnings = indexPatternDatasource.getWarningMessages!(state, framePublicAPI, () => {});
|
||||
|
||||
expect(warnings!.map((item) => (item as React.ReactElement).props.id)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
|
|
|
@ -527,10 +527,10 @@ export function getIndexPatternDatasource({
|
|||
});
|
||||
return messages.length ? messages : undefined;
|
||||
},
|
||||
getWarningMessages: (state, frame) => {
|
||||
getWarningMessages: (state, frame, setState) => {
|
||||
return [
|
||||
...(getStateTimeShiftWarningMessages(state, frame) || []),
|
||||
...getPrecisionErrorWarningMessages(state, frame, core.docLinks),
|
||||
...getPrecisionErrorWarningMessages(state, frame, core.docLinks, setState),
|
||||
];
|
||||
},
|
||||
checkIntegrity: (state) => {
|
||||
|
|
|
@ -109,6 +109,7 @@ export function FieldInputs({
|
|||
label={i18n.translate('xpack.lens.indexPattern.terms.addField', {
|
||||
defaultMessage: 'Add field',
|
||||
})}
|
||||
isDisabled={column.params.orderBy.type === 'rare'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { AggFunctionsMapping } from '../../../../../../../../src/plugins/data/public';
|
||||
import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public';
|
||||
import { updateColumnParam } from '../../layer_helpers';
|
||||
import { updateColumnParam, updateDefaultLabels } from '../../layer_helpers';
|
||||
import type { DataType } from '../../../../types';
|
||||
import { OperationDefinition } from '../index';
|
||||
import { FieldBasedIndexPatternColumn } from '../column_types';
|
||||
|
@ -44,7 +44,15 @@ const missingFieldLabel = i18n.translate('xpack.lens.indexPattern.missingFieldLa
|
|||
defaultMessage: 'Missing field',
|
||||
});
|
||||
|
||||
function ofName(name?: string, count: number = 0) {
|
||||
function ofName(name?: string, count: number = 0, rare: boolean = false) {
|
||||
if (rare) {
|
||||
return i18n.translate('xpack.lens.indexPattern.rareTermsOf', {
|
||||
defaultMessage: 'Rare values of {name}',
|
||||
values: {
|
||||
name: name ?? missingFieldLabel,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (count) {
|
||||
return i18n.translate('xpack.lens.indexPattern.multipleTermsOf', {
|
||||
defaultMessage: 'Top values of {name} + {count} {count, plural, one {other} other {others}}',
|
||||
|
@ -64,6 +72,9 @@ function ofName(name?: string, count: number = 0) {
|
|||
|
||||
const idPrefix = htmlIdGenerator()();
|
||||
const DEFAULT_SIZE = 3;
|
||||
// Elasticsearch limit
|
||||
const MAXIMUM_MAX_DOC_COUNT = 100;
|
||||
export const DEFAULT_MAX_DOC_COUNT = 1;
|
||||
const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']);
|
||||
|
||||
export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field'> = {
|
||||
|
@ -141,6 +152,15 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
};
|
||||
},
|
||||
toEsAggsFn: (column, columnId, _indexPattern, layer, uiSettings, orderedColumnIds) => {
|
||||
if (column.params?.orderBy.type === 'rare') {
|
||||
return buildExpressionFunction<AggFunctionsMapping['aggRareTerms']>('aggRareTerms', {
|
||||
id: columnId,
|
||||
enabled: true,
|
||||
schema: 'segment',
|
||||
field: column.sourceField,
|
||||
max_doc_count: column.params.orderBy.maxDocCount,
|
||||
}).toAst();
|
||||
}
|
||||
if (column.params?.secondaryFields?.length) {
|
||||
return buildExpressionFunction<AggFunctionsMapping['aggMultiTerms']>('aggMultiTerms', {
|
||||
id: columnId,
|
||||
|
@ -183,7 +203,8 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
getDefaultLabel: (column, indexPattern) =>
|
||||
ofName(
|
||||
indexPattern.getFieldByName(column.sourceField)?.displayName,
|
||||
column.params.secondaryFields?.length
|
||||
column.params.secondaryFields?.length,
|
||||
column.params.orderBy.type === 'rare'
|
||||
),
|
||||
onFieldChange: (oldColumn, field) => {
|
||||
// reset the secondary fields
|
||||
|
@ -194,7 +215,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
return {
|
||||
...oldColumn,
|
||||
dataType: field.type as DataType,
|
||||
label: ofName(field.displayName),
|
||||
label: ofName(field.displayName, 0, newParams.orderBy.type === 'rare'),
|
||||
sourceField: field.name,
|
||||
params: newParams,
|
||||
};
|
||||
|
@ -202,7 +223,11 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
onOtherColumnChanged: (layer, thisColumnId, changedColumnId) => {
|
||||
const columns = layer.columns;
|
||||
const currentColumn = columns[thisColumnId] as TermsIndexPatternColumn;
|
||||
if (currentColumn.params.orderBy.type === 'column' || currentColumn.params.orderBy.fallback) {
|
||||
if (
|
||||
currentColumn.params.orderBy.type === 'column' ||
|
||||
(currentColumn.params.orderBy.type === 'alphabetical' &&
|
||||
currentColumn.params.orderBy.fallback)
|
||||
) {
|
||||
// check whether the column is still there and still a metric
|
||||
const columnSortedBy =
|
||||
currentColumn.params.orderBy.type === 'column'
|
||||
|
@ -251,7 +276,11 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
[columnId]: {
|
||||
...column,
|
||||
sourceField: fields[0],
|
||||
label: ofName(indexPattern.getFieldByName(fields[0])?.displayName, fields.length - 1),
|
||||
label: ofName(
|
||||
indexPattern.getFieldByName(fields[0])?.displayName,
|
||||
fields.length - 1,
|
||||
column.params.orderBy.type === 'rare'
|
||||
),
|
||||
params: {
|
||||
...column.params,
|
||||
secondaryFields: fields.length > 1 ? fields.slice(1) : undefined,
|
||||
|
@ -319,7 +348,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
|
||||
const SEPARATOR = '$$$';
|
||||
function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) {
|
||||
if (orderBy.type === 'alphabetical') {
|
||||
if (orderBy.type !== 'column') {
|
||||
return orderBy.type;
|
||||
}
|
||||
return `${orderBy.type}${SEPARATOR}${orderBy.columnId}`;
|
||||
|
@ -329,6 +358,9 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
if (value === 'alphabetical') {
|
||||
return { type: 'alphabetical', fallback: false };
|
||||
}
|
||||
if (value === 'rare') {
|
||||
return { type: 'rare', maxDocCount: DEFAULT_MAX_DOC_COUNT };
|
||||
}
|
||||
const parts = value.split(SEPARATOR);
|
||||
return {
|
||||
type: 'column',
|
||||
|
@ -350,11 +382,20 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
defaultMessage: 'Alphabetical',
|
||||
}),
|
||||
});
|
||||
if (!currentColumn.params.secondaryFields?.length) {
|
||||
orderOptions.push({
|
||||
value: toValue({ type: 'rare', maxDocCount: DEFAULT_MAX_DOC_COUNT }),
|
||||
text: i18n.translate('xpack.lens.indexPattern.terms.orderRare', {
|
||||
defaultMessage: 'Rarity',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ValuesInput
|
||||
value={currentColumn.params.size}
|
||||
disabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
onChange={(value) => {
|
||||
updateLayer(
|
||||
updateColumnParam({
|
||||
|
@ -366,6 +407,25 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
);
|
||||
}}
|
||||
/>
|
||||
{currentColumn.params.orderBy.type === 'rare' && (
|
||||
<ValuesInput
|
||||
value={currentColumn.params.orderBy.maxDocCount}
|
||||
label={i18n.translate('xpack.lens.indexPattern.terms.maxDocCount', {
|
||||
defaultMessage: 'Max doc count per term',
|
||||
})}
|
||||
maxValue={MAXIMUM_MAX_DOC_COUNT}
|
||||
onChange={(value) => {
|
||||
updateLayer(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'orderBy',
|
||||
value: { ...currentColumn.params.orderBy, maxDocCount: value },
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<EuiFormRow
|
||||
label={
|
||||
<>
|
||||
|
@ -396,12 +456,15 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
value={toValue(currentColumn.params.orderBy)}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newOrderByValue = fromValue(e.target.value);
|
||||
const updatedLayer = updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'orderBy',
|
||||
value: newOrderByValue,
|
||||
});
|
||||
const updatedLayer = updateDefaultLabels(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'orderBy',
|
||||
value: newOrderByValue,
|
||||
}),
|
||||
indexPattern
|
||||
);
|
||||
updateLayer(
|
||||
updateColumnParam({
|
||||
layer: updatedLayer,
|
||||
|
@ -434,6 +497,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
|
||||
defaultMessage: 'Rank direction',
|
||||
})}
|
||||
isDisabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
options={[
|
||||
{
|
||||
id: `${idPrefix}asc`,
|
||||
|
@ -468,38 +532,6 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
);
|
||||
}}
|
||||
/>
|
||||
{/* <EuiSelect
|
||||
compressed
|
||||
data-test-subj="indexPattern-terms-orderDirection"
|
||||
options={[
|
||||
{
|
||||
value: 'asc',
|
||||
text: i18n.translate('xpack.lens.indexPattern.terms.orderAscending', {
|
||||
defaultMessage: 'Ascending',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'desc',
|
||||
text: i18n.translate('xpack.lens.indexPattern.terms.orderDescending', {
|
||||
defaultMessage: 'Descending',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
value={currentColumn.params.orderDirection}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
updateLayer(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'orderDirection',
|
||||
value: e.target.value as 'asc' | 'desc',
|
||||
})
|
||||
)
|
||||
}
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderBy', {
|
||||
defaultMessage: 'Rank by',
|
||||
})}
|
||||
/> */}
|
||||
</EuiFormRow>
|
||||
{!hasRestrictions && (
|
||||
<>
|
||||
|
@ -518,6 +550,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
compressed
|
||||
data-test-subj="indexPattern-terms-other-bucket"
|
||||
checked={Boolean(currentColumn.params.otherBucket)}
|
||||
disabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
onChange={(e: EuiSwitchEvent) =>
|
||||
updateLayer(
|
||||
updateColumnParam({
|
||||
|
@ -537,7 +570,8 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
compressed
|
||||
disabled={
|
||||
!currentColumn.params.otherBucket ||
|
||||
indexPattern.getFieldByName(currentColumn.sourceField)?.type !== 'string'
|
||||
indexPattern.getFieldByName(currentColumn.sourceField)?.type !== 'string' ||
|
||||
currentColumn.params.orderBy.type === 'rare'
|
||||
}
|
||||
data-test-subj="indexPattern-terms-missing-bucket"
|
||||
checked={Boolean(currentColumn.params.missingBucket)}
|
||||
|
|
|
@ -67,7 +67,7 @@ describe('terms', () => {
|
|||
columnOrder: ['col1', 'col2'],
|
||||
columns: {
|
||||
col1: {
|
||||
label: 'Top value of category',
|
||||
label: 'Top values of source',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
operationType: 'terms',
|
||||
|
@ -79,7 +79,7 @@ describe('terms', () => {
|
|||
sourceField: 'source',
|
||||
} as TermsIndexPatternColumn,
|
||||
col2: {
|
||||
label: 'Count',
|
||||
label: 'Count of records',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
sourceField: 'Records',
|
||||
|
@ -112,6 +112,30 @@ describe('terms', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should reflect rare terms params correctly', () => {
|
||||
const termsColumn = layer.columns.col1 as TermsIndexPatternColumn;
|
||||
const esAggsFn = termsOperation.toEsAggsFn(
|
||||
{
|
||||
...termsColumn,
|
||||
params: { ...termsColumn.params, orderBy: { type: 'rare', maxDocCount: 3 } },
|
||||
},
|
||||
'col1',
|
||||
{} as IndexPattern,
|
||||
layer,
|
||||
uiSettingsMock,
|
||||
[]
|
||||
);
|
||||
expect(esAggsFn).toEqual(
|
||||
expect.objectContaining({
|
||||
function: 'aggRareTerms',
|
||||
arguments: expect.objectContaining({
|
||||
field: ['source'],
|
||||
max_doc_count: [3],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not enable missing bucket if other bucket is not set', () => {
|
||||
const termsColumn = layer.columns.col1 as TermsIndexPatternColumn;
|
||||
const esAggsFn = termsOperation.toEsAggsFn(
|
||||
|
@ -1379,6 +1403,64 @@ describe('terms', () => {
|
|||
expect(select.prop('disabled')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should disable missing bucket and other bucket setting for rarity sorting', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = shallow(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={{
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn),
|
||||
params: {
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
orderBy: { type: 'rare', maxDocCount: 3 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const select1 = instance
|
||||
.find('[data-test-subj="indexPattern-terms-missing-bucket"]')
|
||||
.find(EuiSwitch);
|
||||
|
||||
expect(select1.prop('disabled')).toEqual(true);
|
||||
|
||||
const select2 = instance
|
||||
.find('[data-test-subj="indexPattern-terms-other-bucket"]')
|
||||
.find(EuiSwitch);
|
||||
|
||||
expect(select2.prop('disabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should disable size input and show max doc count input', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = shallow(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={{
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn),
|
||||
params: {
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
orderBy: { type: 'rare', maxDocCount: 3 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const numberInputs = instance.find(ValuesInput);
|
||||
|
||||
expect(numberInputs).toHaveLength(2);
|
||||
|
||||
expect(numberInputs.first().prop('disabled')).toBeTruthy();
|
||||
expect(numberInputs.last().prop('disabled')).toBeFalsy();
|
||||
expect(numberInputs.last().prop('value')).toEqual(3);
|
||||
});
|
||||
|
||||
it('should disable missing bucket setting if field is not a string', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = shallow(
|
||||
|
@ -1462,6 +1544,7 @@ describe('terms', () => {
|
|||
expect(select.prop('options')!.map(({ value }) => value)).toEqual([
|
||||
'column$$$col2',
|
||||
'alphabetical',
|
||||
'rare',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,10 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn {
|
|||
size: number;
|
||||
// if order is alphabetical, the `fallback` flag indicates whether it became alphabetical because there wasn't
|
||||
// another option or whether the user explicitly chose to make it alphabetical.
|
||||
orderBy: { type: 'alphabetical'; fallback?: boolean } | { type: 'column'; columnId: string };
|
||||
orderBy:
|
||||
| { type: 'alphabetical'; fallback?: boolean }
|
||||
| { type: 'rare'; maxDocCount: number }
|
||||
| { type: 'column'; columnId: string };
|
||||
orderDirection: 'asc' | 'desc';
|
||||
otherBucket?: boolean;
|
||||
missingBucket?: boolean;
|
||||
|
|
|
@ -13,13 +13,20 @@ import { useDebounceWithOptions } from '../../../../shared_components';
|
|||
export const ValuesInput = ({
|
||||
value,
|
||||
onChange,
|
||||
minValue = 1,
|
||||
maxValue = 1000,
|
||||
label = i18n.translate('xpack.lens.indexPattern.terms.size', {
|
||||
defaultMessage: 'Number of values',
|
||||
}),
|
||||
disabled,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
minValue?: number;
|
||||
maxValue?: number;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const MIN_NUMBER_OF_VALUES = 1;
|
||||
const MAX_NUMBER_OF_VALUES = 1000;
|
||||
|
||||
const [inputValue, setInputValue] = useState(String(value));
|
||||
|
||||
useDebounceWithOptions(
|
||||
|
@ -28,7 +35,7 @@ export const ValuesInput = ({
|
|||
return;
|
||||
}
|
||||
const inputNumber = Number(inputValue);
|
||||
onChange(Math.min(MAX_NUMBER_OF_VALUES, Math.max(inputNumber, MIN_NUMBER_OF_VALUES)));
|
||||
onChange(Math.min(maxValue, Math.max(inputNumber, minValue)));
|
||||
},
|
||||
{ skipFirstRender: true },
|
||||
256,
|
||||
|
@ -36,14 +43,12 @@ export const ValuesInput = ({
|
|||
);
|
||||
|
||||
const isEmptyString = inputValue === '';
|
||||
const isHigherThanMax = !isEmptyString && Number(inputValue) > MAX_NUMBER_OF_VALUES;
|
||||
const isLowerThanMin = !isEmptyString && Number(inputValue) < MIN_NUMBER_OF_VALUES;
|
||||
const isHigherThanMax = !isEmptyString && Number(inputValue) > maxValue;
|
||||
const isLowerThanMin = !isEmptyString && Number(inputValue) < minValue;
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.terms.size', {
|
||||
defaultMessage: 'Number of values',
|
||||
})}
|
||||
label={label}
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
isInvalid={isHigherThanMax || isLowerThanMin}
|
||||
|
@ -54,7 +59,7 @@ export const ValuesInput = ({
|
|||
defaultMessage:
|
||||
'Value is higher than the maximum {max}, the maximum value is used instead.',
|
||||
values: {
|
||||
max: MAX_NUMBER_OF_VALUES,
|
||||
max: maxValue,
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
@ -64,7 +69,7 @@ export const ValuesInput = ({
|
|||
defaultMessage:
|
||||
'Value is lower than the minimum {min}, the minimum value is used instead.',
|
||||
values: {
|
||||
min: MIN_NUMBER_OF_VALUES,
|
||||
min: minValue,
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
@ -72,24 +77,21 @@ export const ValuesInput = ({
|
|||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
min={MIN_NUMBER_OF_VALUES}
|
||||
max={MAX_NUMBER_OF_VALUES}
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
step={1}
|
||||
value={inputValue}
|
||||
compressed
|
||||
isInvalid={isHigherThanMax || isLowerThanMin}
|
||||
disabled={disabled}
|
||||
onChange={({ currentTarget }) => setInputValue(currentTarget.value)}
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', {
|
||||
defaultMessage: 'Number of values',
|
||||
})}
|
||||
aria-label={label}
|
||||
onBlur={() => {
|
||||
if (inputValue === '') {
|
||||
return setInputValue(String(value));
|
||||
}
|
||||
const inputNumber = Number(inputValue);
|
||||
setInputValue(
|
||||
String(Math.min(MAX_NUMBER_OF_VALUES, Math.max(inputNumber, MIN_NUMBER_OF_VALUES)))
|
||||
);
|
||||
setInputValue(String(Math.min(maxValue, Math.max(inputNumber, minValue))));
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
|
@ -5,10 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { getPrecisionErrorWarningMessages } from './utils';
|
||||
import type { IndexPatternPrivateState } from './types';
|
||||
import type { IndexPatternPrivateState, GenericIndexPatternColumn } from './types';
|
||||
import type { FramePublicAPI } from '../types';
|
||||
import type { DocLinksStart } from 'kibana/public';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
describe('indexpattern_datasource utils', () => {
|
||||
describe('getPrecisionErrorWarningMessages', () => {
|
||||
|
@ -17,12 +20,34 @@ describe('indexpattern_datasource utils', () => {
|
|||
let docLinks: DocLinksStart;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {} as IndexPatternPrivateState;
|
||||
state = {
|
||||
layers: {
|
||||
id: {
|
||||
indexPatternId: 'one',
|
||||
columns: {
|
||||
col1: {
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
orderBy: {
|
||||
type: 'alphabetical',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
indexPatterns: {
|
||||
one: {
|
||||
getFieldByName: (x: string) => ({ name: x, displayName: x }),
|
||||
},
|
||||
},
|
||||
} as unknown as IndexPatternPrivateState;
|
||||
framePublicAPI = {
|
||||
activeData: {
|
||||
id: {
|
||||
columns: [
|
||||
{
|
||||
id: 'col1',
|
||||
meta: {
|
||||
sourceParams: {
|
||||
hasPrecisionError: false,
|
||||
|
@ -43,19 +68,60 @@ describe('indexpattern_datasource utils', () => {
|
|||
} as DocLinksStart;
|
||||
});
|
||||
test('should not show precisionError if hasPrecisionError is false', () => {
|
||||
expect(getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks)).toHaveLength(0);
|
||||
expect(
|
||||
getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks, () => {})
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should not show precisionError if hasPrecisionError is not defined', () => {
|
||||
delete framePublicAPI.activeData!.id.columns[0].meta.sourceParams!.hasPrecisionError;
|
||||
|
||||
expect(getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks)).toHaveLength(0);
|
||||
expect(
|
||||
getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks, () => {})
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should show precisionError if hasPrecisionError is true', () => {
|
||||
framePublicAPI.activeData!.id.columns[0].meta.sourceParams!.hasPrecisionError = true;
|
||||
|
||||
expect(getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks)).toHaveLength(1);
|
||||
expect(
|
||||
getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks, () => {})
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('if has precision error and sorting is by count ascending, show fix action and switch to rare terms', () => {
|
||||
framePublicAPI.activeData!.id.columns[0].meta.sourceParams!.hasPrecisionError = true;
|
||||
state.layers.id.columnOrder = ['col1', 'col2'];
|
||||
state.layers.id.columns = {
|
||||
col1: {
|
||||
operationType: 'terms',
|
||||
sourceField: 'category',
|
||||
params: {
|
||||
orderBy: {
|
||||
type: 'column',
|
||||
columnId: 'col2',
|
||||
},
|
||||
orderDirection: 'asc',
|
||||
},
|
||||
} as unknown as GenericIndexPatternColumn,
|
||||
col2: {
|
||||
operationType: 'count',
|
||||
} as unknown as GenericIndexPatternColumn,
|
||||
};
|
||||
const setState = jest.fn();
|
||||
const warnings = getPrecisionErrorWarningMessages(state, framePublicAPI, docLinks, setState);
|
||||
|
||||
expect(warnings).toHaveLength(1);
|
||||
const DummyComponent = () => <>{warnings[0]}</>;
|
||||
const warningUi = shallow(<DummyComponent />);
|
||||
warningUi.find(EuiButton).simulate('click');
|
||||
const stateSetter = setState.mock.calls[0][0];
|
||||
const newState = stateSetter(state);
|
||||
expect(newState.layers.id.columns.col1.label).toEqual('Rare values of category');
|
||||
expect(newState.layers.id.columns.col1.params.orderBy).toEqual({
|
||||
type: 'rare',
|
||||
maxDocCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,21 +6,31 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { DocLinksStart } from 'kibana/public';
|
||||
import { EuiLink, EuiTextColor } from '@elastic/eui';
|
||||
import { EuiLink, EuiTextColor, EuiButton, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { DatatableColumn } from 'src/plugins/expressions';
|
||||
import type { FramePublicAPI } from '../types';
|
||||
import type { FramePublicAPI, StateSetter } from '../types';
|
||||
import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from './types';
|
||||
import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/column_types';
|
||||
|
||||
import { operationDefinitionMap, GenericIndexPatternColumn } from './operations';
|
||||
import {
|
||||
operationDefinitionMap,
|
||||
GenericIndexPatternColumn,
|
||||
TermsIndexPatternColumn,
|
||||
CountIndexPatternColumn,
|
||||
updateColumnParam,
|
||||
updateDefaultLabels,
|
||||
} from './operations';
|
||||
|
||||
import { getInvalidFieldMessage } from './operations/definitions/helpers';
|
||||
import { getInvalidFieldMessage, isColumnOfType } from './operations/definitions/helpers';
|
||||
import { isQueryValid } from './operations/definitions/filters';
|
||||
import { checkColumnForPrecisionError } from '../../../../../src/plugins/data/common';
|
||||
import { hasField } from './pure_utils';
|
||||
import { mergeLayer } from './state_helpers';
|
||||
import { DEFAULT_MAX_DOC_COUNT } from './operations/definitions/terms';
|
||||
|
||||
export function isColumnInvalid(
|
||||
layer: IndexPatternLayer,
|
||||
|
@ -80,53 +90,125 @@ export function fieldIsInvalid(
|
|||
export function getPrecisionErrorWarningMessages(
|
||||
state: IndexPatternPrivateState,
|
||||
{ activeData }: FramePublicAPI,
|
||||
docLinks: DocLinksStart
|
||||
docLinks: DocLinksStart,
|
||||
setState: StateSetter<IndexPatternPrivateState>
|
||||
) {
|
||||
const warningMessages: React.ReactNode[] = [];
|
||||
|
||||
if (state && activeData) {
|
||||
Object.values(activeData)
|
||||
.reduce((acc: DatatableColumn[], { columns }) => [...acc, ...columns], [])
|
||||
.forEach((column) => {
|
||||
if (checkColumnForPrecisionError(column)) {
|
||||
warningMessages.push(
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning"
|
||||
defaultMessage="{name} for this visualization may be approximate due to how the data is indexed. Try increasing the number of {topValues} or use {filters} instead of {topValues} for precise results. To learn more about this limit, {link}."
|
||||
values={{
|
||||
name: <EuiTextColor color="accent">{column.name}</EuiTextColor>,
|
||||
topValues: (
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.topValues"
|
||||
defaultMessage="Top values"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
),
|
||||
filters: (
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.filters"
|
||||
defaultMessage="Filters"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
),
|
||||
link: (
|
||||
<EuiLink
|
||||
href={docLinks.links.aggs.terms_doc_count_error}
|
||||
color="text"
|
||||
target="_blank"
|
||||
external={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="visit the documentation"
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.link"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
Object.entries(activeData)
|
||||
.reduce(
|
||||
(acc, [layerId, { columns }]) => [
|
||||
...acc,
|
||||
...columns.map((column) => ({ layerId, column })),
|
||||
],
|
||||
[] as Array<{ layerId: string; column: DatatableColumn }>
|
||||
)
|
||||
.forEach(({ layerId, column }) => {
|
||||
const currentLayer = state.layers[layerId];
|
||||
const currentColumn = currentLayer?.columns[column.id];
|
||||
if (currentLayer && currentColumn && checkColumnForPrecisionError(column)) {
|
||||
const indexPattern = state.indexPatterns[currentLayer.indexPatternId];
|
||||
const isAscendingCountSorting =
|
||||
isColumnOfType<TermsIndexPatternColumn>('terms', currentColumn) &&
|
||||
currentColumn.params.orderBy.type === 'column' &&
|
||||
currentColumn.params.orderDirection === 'asc' &&
|
||||
isColumnOfType<CountIndexPatternColumn>(
|
||||
'count',
|
||||
currentLayer.columns[currentColumn.params.orderBy.columnId]
|
||||
);
|
||||
if (!isAscendingCountSorting) {
|
||||
warningMessages.push(
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning"
|
||||
defaultMessage="{name} for this visualization may be approximate due to how the data is indexed. Try increasing the number of {topValues} or use {filters} instead of {topValues} for precise results. To learn more about this limit, {link}."
|
||||
values={{
|
||||
name: <EuiTextColor color="accent">{column.name}</EuiTextColor>,
|
||||
topValues: (
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.topValues"
|
||||
defaultMessage="Top values"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
),
|
||||
filters: (
|
||||
<EuiTextColor color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.filters"
|
||||
defaultMessage="Filters"
|
||||
/>
|
||||
</EuiTextColor>
|
||||
),
|
||||
link: (
|
||||
<EuiLink
|
||||
href={docLinks.links.aggs.terms_doc_count_error}
|
||||
color="text"
|
||||
target="_blank"
|
||||
external={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="visit the documentation"
|
||||
id="xpack.lens.indexPattern.precisionErrorWarning.link"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
warningMessages.push(
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning"
|
||||
defaultMessage="{name} for this visualization may be approximate due to how the data is indexed. Try sorting by rarity instead of ascending count of records. To learn more about this limit, {link}."
|
||||
values={{
|
||||
name: <EuiTextColor color="accent">{column.name}</EuiTextColor>,
|
||||
link: (
|
||||
<EuiLink
|
||||
href={docLinks.links.aggs.rare_terms}
|
||||
color="text"
|
||||
target="_blank"
|
||||
external={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="visit the documentation"
|
||||
id="xpack.lens.indexPattern.ascendingCountPrecisionErrorWarning.link"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
setState((prevState) =>
|
||||
mergeLayer({
|
||||
state: prevState,
|
||||
layerId,
|
||||
newLayer: updateDefaultLabels(
|
||||
updateColumnParam({
|
||||
layer: currentLayer,
|
||||
columnId: column.id,
|
||||
paramName: 'orderBy',
|
||||
value: {
|
||||
type: 'rare',
|
||||
maxDocCount: DEFAULT_MAX_DOC_COUNT,
|
||||
},
|
||||
}),
|
||||
indexPattern
|
||||
),
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.lens.indexPattern.switchToRare', {
|
||||
defaultMessage: 'Rank by rarity',
|
||||
})}
|
||||
</EuiButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -280,7 +280,11 @@ export interface Datasource<T = unknown, P = unknown> {
|
|||
/**
|
||||
* The frame calls this function to display warnings about visualization
|
||||
*/
|
||||
getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined;
|
||||
getWarningMessages?: (
|
||||
state: T,
|
||||
frame: FramePublicAPI,
|
||||
setState: StateSetter<T>
|
||||
) => React.ReactNode[] | undefined;
|
||||
/**
|
||||
* Checks if the visualization created is time based, for example date histogram
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue