[Vis: Default editor] EUIficate order_agg param editor (#36984) (#37330)

* EUIficate order_agg param editor

* Update browser tests

* Add types

* Update enzyme version in x-pack

* Fix functional tests

* Changes due to comments

* Update_terms_helper.tsx

Co-Authored-By: Maryia Lapata <mary.lopato@gmail.com>

* Fix code review comments

* Update yarn.lock
This commit is contained in:
Maryia Lapata 2019-05-29 17:09:45 +03:00 committed by GitHub
parent e10c35156f
commit 8791531896
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 394 additions and 247 deletions

View file

@ -47,84 +47,8 @@ describe('Terms Agg', function () {
});
}
it('defaults to the first metric agg', function () {
init({
responseValueAggs: [
{
id: 'agg1',
type: {
name: 'count'
}
},
{
id: 'agg2',
type: {
name: 'count'
}
}
]
});
expect($rootScope.agg.params.orderBy).to.be('agg1');
});
it('defaults to the first metric agg that is compatible with the terms bucket', function () {
init({
responseValueAggs: [
{
id: 'agg1',
type: {
name: 'top_hits'
}
},
{
id: 'agg2',
type: {
name: 'percentiles'
}
},
{
id: 'agg3',
type: {
name: 'median'
}
},
{
id: 'agg4',
type: {
name: 'std_dev'
}
},
{
id: 'agg5',
type: {
name: 'count'
}
}
]
});
expect($rootScope.agg.params.orderBy).to.be('agg5');
});
it('defaults to the _key metric if no agg is compatible', function () {
init({
responseValueAggs: [
{
id: 'agg1',
type: {
name: 'top_hits'
}
}
]
});
expect($rootScope.agg.params.orderBy).to.be('_key');
});
it('selects _key if there are no metric aggs', function () {
init({});
expect($rootScope.agg.params.orderBy).to.be('_key');
});
it('selects _key if the selected metric becomes incompatible', function () {
// should be rewritten after EUIficate order_agg.html
it.skip('selects _key if the selected metric becomes incompatible', function () {
init({
responseValueAggs: [
{
@ -148,36 +72,8 @@ describe('Terms Agg', function () {
expect($rootScope.agg.params.orderBy).to.be('_key');
});
it('selects first metric if it is avg', function () {
init({
responseValueAggs: [
{
id: 'agg1',
type: {
name: 'avg',
field: 'bytes'
}
}
]
});
expect($rootScope.agg.params.orderBy).to.be('agg1');
});
it('selects _key if the first metric is avg_bucket', function () {
$rootScope.responseValueAggs = [
{
id: 'agg1',
type: {
name: 'avg_bucket',
metric: 'custom'
}
}
];
$rootScope.$digest();
expect($rootScope.agg.params.orderBy).to.be('_key');
});
it('selects _key if the selected metric is removed', function () {
// should be rewritten after EUIficate order_agg.html
it.skip('selects _key if the selected metric is removed', function () {
init({
responseValueAggs: [
{

View file

@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { AggConfig } from 'ui/vis';
import { i18n } from '@kbn/i18n';
const aggFilter = [
'!top_hits',
'!percentiles',
'!median',
'!std_dev',
'!derivative',
'!moving_avg',
'!serial_diff',
'!cumulative_sum',
'!avg_bucket',
'!max_bucket',
'!min_bucket',
'!sum_bucket',
];
// Returns true if the agg is compatible with the terms bucket
function isCompatibleAgg(agg: AggConfig) {
return !aggFilter.includes(`!${agg.type.name}`);
}
function safeMakeLabel(agg: AggConfig) {
try {
return agg.makeLabel();
} catch (e) {
return i18n.translate('common.ui.aggTypes.buckets.terms.aggNotValidLabel', {
defaultMessage: '- agg not valid -',
});
}
}
export { aggFilter, isCompatibleAgg, safeMakeLabel };

View file

@ -17,29 +17,23 @@
* under the License.
*/
import _ from 'lodash';
import chrome from 'ui/chrome';
import { i18n } from '@kbn/i18n';
import { BucketAggType } from './_bucket_agg_type';
import { AggConfig } from '../../vis/agg_config';
import { Schemas } from '../../vis/editors/default/schemas';
import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils';
import { createFilterTerms } from './create_filter/terms';
import { wrapWithInlineComp } from './_inline_comp_wrapper';
import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } from './_terms_other_bucket_helper';
import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format';
import { aggFilter } from './_terms_helper';
import orderAggTemplate from '../controls/order_agg.html';
import { OrderParamEditor } from '../controls/order';
import { OrderAggParamEditor } from '../controls/order_agg';
import { SizeParamEditor } from '../controls/size';
import { wrapWithInlineComp } from './_inline_comp_wrapper';
import { i18n } from '@kbn/i18n';
import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils';
import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } from './_terms_other_bucket_helper';
import { MissingBucketParamEditor } from '../controls/missing_bucket';
import { OtherBucketParamEditor } from '../controls/other_bucket';
import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format';
const aggFilter = [
'!top_hits', '!percentiles', '!median', '!std_dev',
'!derivative', '!moving_avg', '!serial_diff', '!cumulative_sum',
'!avg_bucket', '!max_bucket', '!min_bucket', '!sum_bucket'
];
const orderAggSchema = (new Schemas([
{
@ -120,6 +114,11 @@ export const termsBucketAgg = new BucketAggType({
type: 'field',
filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string']
},
{
name: 'orderBy',
editorComponent: OrderAggParamEditor,
write: () => {} // prevent default write, it's handled by orderAgg
},
{
name: 'orderAgg',
type: AggConfig,
@ -139,33 +138,9 @@ export const termsBucketAgg = new BucketAggType({
return orderAgg;
},
controller: function ($scope) {
$scope.safeMakeLabel = function (agg) {
try {
return agg.makeLabel();
} catch (e) {
return i18n.translate('common.ui.aggTypes.buckets.terms.aggNotValidLabel', {
defaultMessage: '- agg not valid -',
});
}
};
const INIT = {}; // flag to know when prevOrderBy has changed
let prevOrderBy = INIT;
$scope.$watch('responseValueAggs', updateOrderAgg);
$scope.$watch('agg.params.orderBy', updateOrderAgg);
// Returns true if the agg is not compatible with the terms bucket
$scope.rejectAgg = function rejectAgg(agg) {
return aggFilter.includes(`!${agg.type.name}`);
};
$scope.$watch('agg.params.field.type', (type) => {
if (type !== 'string') {
$scope.agg.params.missingBucket = false;
}
});
function updateOrderAgg() {
// abort until we get the responseValueAggs
if (!$scope.responseValueAggs) return;
@ -174,27 +149,9 @@ export const termsBucketAgg = new BucketAggType({
const orderBy = params.orderBy;
const paramDef = agg.type.params.byName.orderAgg;
// setup the initial value of orderBy
if (!orderBy && prevOrderBy === INIT) {
let respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).first();
if (!respAgg) {
respAgg = { id: '_key' };
}
params.orderBy = respAgg.id;
return;
}
// track the previous value
prevOrderBy = orderBy;
// we aren't creating a custom aggConfig
if (!orderBy || orderBy !== 'custom') {
params.orderAgg = null;
// ensure that orderBy is set to a valid agg
const respAgg = _($scope.responseValueAggs).filter((agg) => !$scope.rejectAgg(agg)).find({ id: orderBy });
if (!respAgg) {
params.orderBy = '_key';
}
return;
}
@ -256,22 +213,18 @@ export const termsBucketAgg = new BucketAggType({
value: 'asc'
}
],
write: _.noop // prevent default write, it's handled by orderAgg
write: () => {} // prevent default write, it's handled by orderAgg
},
{
name: 'size',
editorComponent: wrapWithInlineComp(SizeParamEditor),
default: 5
},
{
name: 'orderBy',
write: _.noop // prevent default write, it's handled by orderAgg
},
{
name: 'otherBucket',
default: false,
editorComponent: OtherBucketParamEditor,
write: _.noop,
write: () => {},
},
{
name: 'otherBucketLabel',
@ -283,13 +236,13 @@ export const termsBucketAgg = new BucketAggType({
defaultMessage: 'Label for other bucket',
}),
shouldShow: agg => agg.params.otherBucket,
write: _.noop,
write: () => {},
},
{
name: 'missingBucket',
default: false,
editorComponent: MissingBucketParamEditor,
write: _.noop,
write: () => {},
},
{
name: 'missingBucketLabel',
@ -303,7 +256,7 @@ export const termsBucketAgg = new BucketAggType({
defaultMessage: 'Label for missing values',
}),
shouldShow: agg => agg.params.missingBucket,
write: _.noop,
write: () => {},
},
{
name: 'exclude',

View file

@ -17,13 +17,24 @@
* under the License.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { AggParamEditorProps } from 'ui/vis/editors/default';
import { SwitchParamEditor } from './switch';
import { isStringType } from '../buckets/migrate_include_exclude_format';
function MissingBucketParamEditor(props: AggParamEditorProps<boolean>) {
const fieldTypeIsNotString = !isStringType(props.agg);
useEffect(
() => {
if (fieldTypeIsNotString) {
props.setValue(false);
}
},
[fieldTypeIsNotString]
);
return (
<SwitchParamEditor
dataTestSubj="missingBucketSwitch"
@ -37,7 +48,7 @@ function MissingBucketParamEditor(props: AggParamEditorProps<boolean>) {
'If not in the top N, and you enable "Group other values in separate bucket", ' +
'Elasticsearch adds the missing values to the "other" bucket.',
})}
disabled={!isStringType(props.agg)}
disabled={fieldTypeIsNotString}
{...props}
/>
);

View file

@ -1,49 +1,12 @@
<div ng-controller="aggParam.controller">
<div class="form-group">
<label
for="visEditorOrder{{agg.id}}"
i18n-id="common.ui.aggTypes.orderAgg.orderByLabel"
i18n-default-message="Order By"
></label>
<select
id="visEditorOrder{{agg.id}}"
name="orderBy"
ng-model="agg.params.orderBy"
required
class="form-control">
<option
ng-repeat="respAgg in responseValueAggs track by respAgg.id"
value="{{respAgg.id}}"
data-test-subj="visEditorOrder{{agg.id}}-{{respAgg.id}}"
ng-disabled="rejectAgg(respAgg)"
ng-selected="agg.params.orderBy === respAgg.id"
i18n-id="common.ui.aggTypes.orderAgg.metricLabel"
i18n-default-message="metric: {metric}"
i18n-values="{ metric: safeMakeLabel(respAgg) }"
>
</option>
<option
value="custom"
data-test-subj="visEditorOrder{{agg.id}}-custom"
ng-selected="agg.params.orderBy === 'custom'"
i18n-id="common.ui.aggTypes.orderAgg.customMetricLabel"
i18n-default-message="Custom Metric"
></option>
<option
value="_key"
data-test-subj="visEditorOrder{{agg.id}}-key"
ng-selected="agg.params.orderBy === '_key'"
i18n-id="common.ui.aggTypes.orderAgg.alphabeticalLabel"
i18n-default-message="Alphabetical"
></option>
</select>
</div>
<div ng-show="agg.params.orderAgg" class="visEditorAgg__subAgg">
<vis-editor-agg-params
index-pattern="agg.getIndexPattern()"
agg="agg.params.orderAgg"
ng-if="agg.params.orderAgg"
group-name="'metrics'">
</vis-editor-agg-params>
</div>
<div
ng-controller="aggParam.controller"
ng-show="agg.params.orderAgg"
class="visEditorAgg__subAgg"
>
<vis-editor-agg-params
index-pattern="agg.getIndexPattern()"
agg="agg.params.orderAgg"
ng-if="agg.params.orderAgg"
group-name="'metrics'">
</vis-editor-agg-params>
</div>

View file

@ -0,0 +1,167 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { OrderAggParamEditor } from './order_agg';
describe('OrderAggParamEditor component', () => {
let setValue: jest.Mock;
let setValidity: jest.Mock;
let setTouched: jest.Mock;
let defaultProps: any;
beforeEach(() => {
setValue = jest.fn();
setValidity = jest.fn();
setTouched = jest.fn();
defaultProps = {
agg: {},
aggParam: {
name: 'orderAgg',
type: '',
},
editorConfig: {},
value: '',
showValidation: false,
setValue,
setValidity,
setTouched,
};
});
it('defaults to the first metric agg after init', () => {
const responseValueAggs = [
{
id: 'agg1',
type: {
name: 'count',
},
},
{
id: 'agg2',
type: {
name: 'count',
},
},
];
const props = { ...defaultProps, responseValueAggs };
mount(<OrderAggParamEditor {...props} />);
expect(setValue).toHaveBeenCalledWith('agg1');
});
it('defaults to the first metric agg that is compatible with the terms bucket', () => {
const responseValueAggs = [
{
id: 'agg1',
type: {
name: 'top_hits',
},
},
{
id: 'agg2',
type: {
name: 'percentiles',
},
},
{
id: 'agg3',
type: {
name: 'median',
},
},
{
id: 'agg4',
type: {
name: 'std_dev',
},
},
{
id: 'agg5',
type: {
name: 'count',
},
},
];
const props = { ...defaultProps, responseValueAggs };
mount(<OrderAggParamEditor {...props} />);
expect(setValue).toHaveBeenCalledWith('agg5');
});
it('defaults to the _key metric if no agg is compatible', () => {
const responseValueAggs = [
{
id: 'agg1',
type: {
name: 'top_hits',
},
},
];
const props = { ...defaultProps, responseValueAggs };
mount(<OrderAggParamEditor {...props} />);
expect(setValue).toHaveBeenCalledWith('_key');
});
it('selects first metric if it is avg', () => {
const responseValueAggs = [
{
id: 'agg1',
type: {
name: 'avg',
field: 'bytes',
},
},
];
const props = { ...defaultProps, responseValueAggs };
mount(<OrderAggParamEditor {...props} />);
expect(setValue).toHaveBeenCalledWith('agg1');
});
it('selects _key if the first metric is avg_bucket', () => {
const responseValueAggs = [
{
id: 'agg1',
type: {
name: 'avg_bucket',
metric: 'custom',
},
},
];
const props = { ...defaultProps, responseValueAggs };
mount(<OrderAggParamEditor {...props} />);
expect(setValue).toHaveBeenCalledWith('_key');
});
it('selects _key if there are no metric aggs', () => {
mount(<OrderAggParamEditor {...defaultProps} />);
expect(setValue).toHaveBeenCalledWith('_key');
});
});

View file

@ -0,0 +1,119 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect } from 'react';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AggParamEditorProps } from 'ui/vis/editors/default';
import { safeMakeLabel, isCompatibleAgg } from '../buckets/_terms_helper';
function OrderAggParamEditor({
agg,
value,
showValidation,
setValue,
setValidity,
setTouched,
responseValueAggs,
}: AggParamEditorProps<string>) {
const label = i18n.translate('common.ui.aggTypes.orderAgg.orderByLabel', {
defaultMessage: 'Order by',
});
const isValid = !!value;
useEffect(
() => {
setValidity(isValid);
},
[isValid]
);
useEffect(() => {
// setup the initial value of orderBy
if (!value) {
let respAgg = { id: '_key' };
if (responseValueAggs) {
respAgg = responseValueAggs.filter(isCompatibleAgg)[0] || respAgg;
}
setValue(respAgg.id);
}
}, []);
useEffect(
() => {
if (responseValueAggs && value && value !== 'custom') {
// ensure that orderBy is set to a valid agg
const respAgg = responseValueAggs
.filter(isCompatibleAgg)
.find(aggregation => aggregation.id === value);
if (!respAgg) {
setValue('_key');
}
}
},
[responseValueAggs]
);
const defaultOptions = [
{
text: i18n.translate('common.ui.aggTypes.orderAgg.customMetricLabel', {
defaultMessage: 'Custom metric',
}),
value: 'custom',
},
{
text: i18n.translate('common.ui.aggTypes.orderAgg.alphabeticalLabel', {
defaultMessage: 'Alphabetical',
}),
value: '_key',
},
];
const options = responseValueAggs
? responseValueAggs.map(respAgg => ({
text: i18n.translate('common.ui.aggTypes.orderAgg.metricLabel', {
defaultMessage: 'Metric: {metric}',
values: {
metric: safeMakeLabel(respAgg),
},
}),
value: respAgg.id,
disabled: !isCompatibleAgg(respAgg),
}))
: [];
return (
<EuiFormRow label={label} fullWidth={true} isInvalid={showValidation ? !isValid : false}>
<EuiSelect
options={[...options, ...defaultOptions]}
value={value}
onChange={ev => setValue(ev.target.value)}
fullWidth={true}
isInvalid={showValidation ? !isValid : false}
onBlur={setTouched}
data-test-subj={`visEditorOrderBy${agg.id}`}
/>
</EuiFormRow>
);
}
export { OrderAggParamEditor };

View file

@ -34,6 +34,7 @@ uiModules
['onChange', { watchDepth: 'reference' }],
['setTouched', { watchDepth: 'reference' }],
['setValidity', { watchDepth: 'reference' }],
['responseValueAggs', { watchDepth: 'reference' }],
'showValidation',
'value',
'visName'
@ -69,6 +70,7 @@ uiModules
on-change="onChange"
set-touched="setTouched"
set-validity="setValidity"
response-value-aggs="responseValueAggs"
></vis-agg-param-react-wrapper>`;
}

View file

@ -35,6 +35,7 @@ export interface AggParamCommonProps<T> {
indexedFields?: FieldParamType[];
showValidation: boolean;
value: T;
responseValueAggs: AggConfig[] | null;
visName: string;
setValidity(isValid: boolean): void;
setTouched(): void;

View file

@ -86,7 +86,7 @@ export default function ({ getService, getPageObjects }) {
const expectedChartData = ['png 1,373', 'php 445', 'jpg 9,109', 'gif 918', 'css 2,159'];
log.debug('Order By = Term');
await PageObjects.visualize.selectOrderBy('_key');
await PageObjects.visualize.selectOrderByMetric(2, '_key');
await PageObjects.visualize.clickGo();
await retry.try(async function () {
const data = await PageObjects.visualize.getLineChartData();

View file

@ -49,7 +49,7 @@ export default function ({ getService, getPageObjects }) {
await retry.try(async function tryingForTime() {
await PageObjects.visualize.selectField(termsField);
});
await PageObjects.visualize.selectOrderBy('_key');
await PageObjects.visualize.selectOrderByMetric(2, '_key');
await PageObjects.visualize.clickGo();
});

View file

@ -550,21 +550,6 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
await find.clickByCssSelector(`#${id} > option[label="${fieldValue}"]`);
}
async orderBy(fieldValue) {
await find.clickByCssSelector(
'select.form-control.ng-pristine.ng-valid.ng-untouched.ng-valid-required[ng-model="agg.params.orderBy"]'
+ `option:contains("${fieldValue}")`);
}
async selectOrderBy(fieldValue) {
await find.clickByCssSelector(`select[name="orderBy"] > option[value="${fieldValue}"]`);
}
async getInputTypeParam(paramName) {
const input = await find.byCssSelector(`input[ng-model="agg.params.${paramName}"]`);
return await input.getProperty('value');
}
async getInterval() {
return await comboBox.getComboBoxSelectedOptions('visEditorInterval');
}
@ -1237,13 +1222,14 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
return errorMessage;
}
async selectSortMetric(agg, metric) {
const sortMetric = await find.byCssSelector(`[data-test-subj="visEditorOrder${agg}-${metric}"]`);
return await sortMetric.click();
async selectOrderByMetric(agg, metric) {
const sortSelect = await testSubjects.find(`visEditorOrderBy${agg}`);
const sortMetric = await sortSelect.findByCssSelector(`option[value="${metric}"]`);
await sortMetric.click();
}
async selectCustomSortMetric(agg, metric, field) {
await this.selectSortMetric(agg, 'custom');
await this.selectOrderByMetric(agg, 'custom');
await this.selectAggregation(metric, 'groupName');
await this.selectField(field, 'groupName');
}

View file

@ -230,9 +230,7 @@
"common.ui.aggTypes.onlyRequestDataAroundMapExtentLabel": "マップ範囲のデータのみリクエストしてください",
"common.ui.aggTypes.onlyRequestDataAroundMapExtentTooltip": "Apply geo_bounding_box filter aggregation to narrow the subject area to the map view box with collar",
"common.ui.aggTypes.orderAgg.alphabeticalLabel": "アルファベット順",
"common.ui.aggTypes.orderAgg.customMetricLabel": "カスタムメトリック",
"common.ui.aggTypes.orderAgg.metricLabel": "メトリック: {metric}",
"common.ui.aggTypes.orderAgg.orderByLabel": "並び順",
"common.ui.aggTypes.orderLabel": "順序",
"common.ui.aggTypes.otherBucket.groupValuesLabel": "他の値を別のバケットにまとめる",
"common.ui.aggTypes.otherBucket.groupValuesTooltip": "トップ N 以外の値はこのバケットにまとめられます。欠測値があるドキュメントを含めるには、「欠測値を表示」を有効にしてください。",

View file

@ -230,9 +230,7 @@
"common.ui.aggTypes.onlyRequestDataAroundMapExtentLabel": "仅请求地图范围的数据",
"common.ui.aggTypes.onlyRequestDataAroundMapExtentTooltip": "应用 geo_bounding_box 筛选聚合以使用领口将主题区域缩小到地图视图框",
"common.ui.aggTypes.orderAgg.alphabeticalLabel": "按字母顺序",
"common.ui.aggTypes.orderAgg.customMetricLabel": "定制指标",
"common.ui.aggTypes.orderAgg.metricLabel": "指标:{metric}",
"common.ui.aggTypes.orderAgg.orderByLabel": "排序依据",
"common.ui.aggTypes.orderLabel": "顺序",
"common.ui.aggTypes.otherBucket.groupValuesLabel": "在单独的存储桶中对其他值分组",
"common.ui.aggTypes.otherBucket.groupValuesTooltip": "不在排名前 N 中的值将在此存储桶中进行分组。要包括缺失值的文档,请启用“显示缺失值”。",