[Discover] Remove legacy field stats (#155503)

Closes https://github.com/elastic/kibana/issues/154841

## Summary

This PR removes legacy field stats which were shown in field popover
before. They were based on loaded hits and were less precise than the
current default field stats (they are based now on 5000 documents per
shard sample).

Before (opt-in option):
<img width="500" alt="Screenshot 2023-04-21 at 14 26 57"
src="https://user-images.githubusercontent.com/1415710/233635136-ebc4017e-71c3-499d-96ac-544e26ea4b49.png">

After:
<img width="500" alt="Screenshot 2023-04-21 at 14 26 20"
src="https://user-images.githubusercontent.com/1415710/233635194-8c60fd05-5db6-40e0-abcd-672689932a83.png">
This commit is contained in:
Julia Rechkunova 2023-04-27 17:57:08 +02:00 committed by GitHub
parent 454418ba8e
commit c6b6e28f2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 14 additions and 940 deletions

View file

@ -28,7 +28,6 @@ export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight';
export const ROW_HEIGHT_OPTION = 'discover:rowHeightOption';
export const SEARCH_EMBEDDABLE_TYPE = 'search';
export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements';
export const SHOW_LEGACY_FIELD_TOP_VALUES = 'discover:showLegacyFieldTopValues';
export const ENABLE_SQL = 'discover:enableSql';
export { DISCOVER_APP_LOCATOR, DiscoverAppLocatorDefinition } from './locator';

View file

@ -1,4 +0,0 @@
.dscFieldDetails__barContainer {
// Constrains value to the flex item, and allows for truncation when necessary
min-width: 0;
}

View file

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 React from 'react';
import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DataViewField } from '@kbn/data-views-plugin/public';
import { StringFieldProgressBar } from './string_progress_bar';
import { Bucket } from './types';
import './discover_field_bucket.scss';
interface Props {
bucket: Bucket;
field: DataViewField;
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
const emptyTxt = i18n.translate('discover.fieldChooser.detailViews.emptyStringText', {
defaultMessage: 'Empty string',
});
const addLabel = i18n.translate('discover.fieldChooser.detailViews.filterValueButtonAriaLabel', {
defaultMessage: 'Filter for {field}: "{value}"',
values: { value: bucket.value, field: field.name },
});
const removeLabel = i18n.translate(
'discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel',
{
defaultMessage: 'Filter out {field}: "{value}"',
values: { value: bucket.value, field: field.name },
}
);
return (
<>
<EuiFlexGroup justifyContent="spaceBetween" responsive={false} gutterSize="s">
<EuiFlexItem className="dscFieldDetails__barContainer" grow={1}>
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={1} className="eui-textTruncate">
<EuiText
title={
bucket.display === ''
? emptyTxt
: `${bucket.display}: ${bucket.count} (${bucket.percent}%)`
}
size="xs"
className="eui-textTruncate"
>
{bucket.display === '' ? emptyTxt : bucket.display}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false} className="eui-textTruncate">
<EuiText color="success" size="xs" className="eui-textTruncate">
{bucket.percent}%
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<StringFieldProgressBar
value={bucket.value}
percent={bucket.percent}
count={bucket.count}
/>
</EuiFlexItem>
{onAddFilter && field.filterable && (
<EuiFlexItem grow={false}>
<div>
<EuiButtonIcon
iconSize="s"
iconType="plusInCircle"
onClick={() => onAddFilter(field, bucket.value, '+')}
aria-label={addLabel}
data-test-subj={`plus-${field.name}-${bucket.value}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
paddingRight: 2,
paddingLeft: 2,
paddingTop: 0,
paddingBottom: 0,
}}
/>
<EuiButtonIcon
iconSize="s"
iconType="minusInCircle"
onClick={() => onAddFilter(field, bucket.value, '-')}
aria-label={removeLabel}
data-test-subj={`minus-${field.name}-${bucket.value}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
paddingTop: 0,
paddingBottom: 0,
paddingRight: 2,
paddingLeft: 2,
}}
/>
</div>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
);
}

View file

@ -1,110 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 React from 'react';
import { findTestSubject } from '@elastic/eui/lib/test';
import { ReactWrapper } from 'enzyme';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { EuiLoadingSpinner } from '@elastic/eui';
import { act } from 'react-dom/test-utils';
import { DiscoverFieldDetails } from './discover_field_details';
import { DataViewField } from '@kbn/data-views-plugin/public';
import { stubDataView, stubLogstashDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { BehaviorSubject } from 'rxjs';
import { FetchStatus } from '../../../../types';
import { DataDocuments$ } from '../../../services/discover_data_state_container';
import { getDataTableRecords } from '../../../../../__fixtures__/real_hits';
describe('discover sidebar field details', function () {
const onAddFilter = jest.fn();
const defaultProps = {
dataView: stubDataView,
details: { buckets: [], error: '', exists: 1, total: 2, columns: [] },
onAddFilter,
};
const hits = getDataTableRecords(stubLogstashDataView);
const documents$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: hits,
}) as DataDocuments$;
function mountComponent(field: DataViewField) {
const compProps = { ...defaultProps, field, documents$ };
return mountWithIntl(<DiscoverFieldDetails {...compProps} />);
}
it('click on addFilter calls the function', function () {
const visualizableField = new DataViewField({
name: 'bytes',
type: 'number',
esTypes: ['long'],
count: 10,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
});
const component = mountComponent(visualizableField);
const onAddButton = findTestSubject(component, 'onAddFilterButton');
onAddButton.simulate('click');
expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+');
});
it('should stay in sync with documents$ state', async function () {
const testDocuments$ = new BehaviorSubject({
fetchStatus: FetchStatus.LOADING,
}) as DataDocuments$;
const visualizableField = new DataViewField({
name: 'bytes',
type: 'number',
esTypes: ['long'],
count: 10,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
});
let component: ReactWrapper;
await act(async () => {
component = await mountWithIntl(
<DiscoverFieldDetails
{...defaultProps}
field={visualizableField}
documents$={testDocuments$}
/>
);
});
expect(component!.find(EuiLoadingSpinner).exists()).toBeTruthy();
await act(async () => {
testDocuments$.next({
fetchStatus: FetchStatus.COMPLETE,
result: hits,
});
});
await component!.update();
expect(component!.find(EuiLoadingSpinner).exists()).toBeFalsy();
expect(
findTestSubject(component!, `discoverFieldDetails-${visualizableField.name}`).exists()
).toBeTruthy();
await act(async () => {
testDocuments$.next({
fetchStatus: FetchStatus.UNINITIALIZED,
});
});
await component!.update();
expect(component!.isEmptyRender()).toBeTruthy();
});
});

View file

@ -1,117 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiLink, EuiSpacer, EuiText, EuiTitle, EuiLoadingSpinner } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { DiscoverFieldBucket } from './discover_field_bucket';
import { Bucket, FieldDetails } from './types';
import { getDetails, isValidFieldDetails } from './get_details';
import { FetchStatus } from '../../../../types';
import { DataDocuments$ } from '../../../services/discover_data_state_container';
interface DiscoverFieldDetailsProps {
/**
* hits fetched from ES, displayed in the doc table
*/
documents$: DataDocuments$;
field: DataViewField;
dataView: DataView;
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldDetails({
documents$,
field,
dataView,
onAddFilter,
}: DiscoverFieldDetailsProps) {
const [detailsState, setDetailsState] = useState<{
details?: FieldDetails;
loaded: boolean;
}>();
useEffect(() => {
const subscription = documents$.subscribe((data) => {
if (data.fetchStatus === FetchStatus.COMPLETE) {
setDetailsState({ details: getDetails(field, data.result, dataView), loaded: true });
} else {
setDetailsState({ details: undefined, loaded: data.fetchStatus !== FetchStatus.LOADING });
}
});
return () => {
subscription.unsubscribe();
};
}, [documents$, setDetailsState, dataView, field]);
if (!detailsState?.loaded) {
return <EuiLoadingSpinner />;
}
const details = detailsState?.details;
if (!details) {
return null;
}
return (
<div data-test-subj={`discoverFieldDetails-${field.name}`}>
<EuiTitle size="xxxs">
<h5>
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
defaultMessage: 'Top 5 values',
})}
</h5>
</EuiTitle>
{!isValidFieldDetails(details) && <EuiText size="xs">{details.error}</EuiText>}
{isValidFieldDetails(details) && (
<>
<div style={{ marginTop: '4px' }}>
{details.buckets.map((bucket: Bucket, idx: number) => (
<DiscoverFieldBucket
key={`bucket${idx}`}
bucket={bucket}
field={field}
onAddFilter={onAddFilter}
/>
))}
</div>
<EuiSpacer size="xs" />
<EuiText size="xs">
{onAddFilter && !dataView.metaFields.includes(field.name) && !field.scripted ? (
<EuiLink
onClick={() => onAddFilter('_exists_', field.name, '+')}
data-test-subj="onAddFilterButton"
>
<FormattedMessage
id="discover.fieldChooser.detailViews.existsInRecordsText"
defaultMessage="Exists in {value} / {totalValue} records"
values={{
value: details.exists,
totalValue: details.total,
}}
/>
</EuiLink>
) : (
<FormattedMessage
id="discover.fieldChooser.detailViews.valueOfRecordsText"
defaultMessage="{value} / {totalValue} records"
values={{
value: details.exists,
totalValue: details.total,
}}
/>
)}
</EuiText>
</>
)}
</div>
);
}

View file

@ -1,199 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { keys, clone, uniq, filter, map } from 'lodash';
import { getDataTableRecords } from '../../../../../__fixtures__/real_hits';
import { fieldCalculator, FieldCountsParams } from './field_calculator';
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { FieldDetails, ValidFieldDetails } from './types';
import { isValidFieldDetails } from './get_details';
const validateResults = (
extensions: FieldDetails,
validate: (extensions: ValidFieldDetails) => void
) => {
if (isValidFieldDetails(extensions)) {
validate(extensions);
} else {
throw new Error('extensions is not valid');
}
};
describe('fieldCalculator', function () {
it('should have a _countMissing that counts nulls & undefineds in an array', function () {
const values = [
['foo', 'bar'],
'foo',
'foo',
undefined,
['foo', 'bar'],
'bar',
'baz',
null,
null,
null,
'foo',
undefined,
];
expect(fieldCalculator._countMissing(values)).toBe(5);
});
describe('_groupValues', function () {
let groups: Record<string, any>;
let params: any;
let values: any;
beforeEach(function () {
values = [
['foo', 'bar'],
'foo',
'foo',
undefined,
['foo', 'bar'],
'bar',
'baz',
null,
null,
null,
'foo',
undefined,
];
params = {};
groups = fieldCalculator._groupValues(values, params);
});
it('should have a _groupValues that counts values', function () {
expect(groups).toBeInstanceOf(Object);
});
it('should throw an error if any value is a plain object', function () {
expect(function () {
fieldCalculator._groupValues([{}, true, false], params);
}).toThrowError();
});
it('should handle values with dots in them', function () {
values = ['0', '0.........', '0.......,.....'];
params = {};
groups = fieldCalculator._groupValues(values, params);
expect(groups[values[0]].count).toBe(1);
expect(groups[values[1]].count).toBe(1);
expect(groups[values[2]].count).toBe(1);
});
it('should have a a key for value in the array when not grouping array terms', function () {
expect(keys(groups).length).toBe(3);
expect(groups.foo).toBeInstanceOf(Object);
expect(groups.bar).toBeInstanceOf(Object);
expect(groups.baz).toBeInstanceOf(Object);
});
it('should count array terms independently', function () {
expect(groups['foo,bar']).toBe(undefined);
expect(groups.foo.count).toBe(5);
expect(groups.bar.count).toBe(3);
expect(groups.baz.count).toBe(1);
});
describe('grouped array terms', function () {
beforeEach(function () {
params.grouped = true;
groups = fieldCalculator._groupValues(values, params);
});
it('should group array terms when passed params.grouped', function () {
expect(keys(groups).length).toBe(4);
expect(groups['foo,bar']).toBeInstanceOf(Object);
});
it('should contain the original array as the value', function () {
expect(groups['foo,bar'].value).toEqual(['foo', 'bar']);
});
it('should count the pairs separately from the values they contain', function () {
expect(groups['foo,bar'].count).toBe(2);
expect(groups.foo.count).toBe(3);
expect(groups.bar.count).toBe(1);
});
});
});
describe('getFieldValues', function () {
let hits: any;
beforeEach(function () {
hits = getDataTableRecords(dataView);
});
it('Should return an array of values for _source fields', function () {
const extensions = fieldCalculator.getFieldValues(
hits,
dataView.fields.getByName('extension')!
);
expect(extensions).toBeInstanceOf(Array);
expect(filter(extensions, (v) => v === 'html').length).toBe(8);
expect(uniq(clone(extensions)).sort()).toEqual(['gif', 'html', 'php', 'png']);
});
it('Should return an array of values for core meta fields', function () {
const types = fieldCalculator.getFieldValues(hits, dataView.fields.getByName('_id')!);
expect(types).toBeInstanceOf(Array);
expect(types.length).toBe(20);
});
});
describe('getFieldValueCounts', function () {
let params: FieldCountsParams;
beforeEach(function () {
params = {
hits: getDataTableRecords(dataView),
field: dataView.fields.getByName('extension')!,
count: 3,
dataView,
};
});
it('counts the top 3 values', function () {
validateResults(fieldCalculator.getFieldValueCounts(params), (extensions) => {
expect(extensions).toBeInstanceOf(Object);
expect(extensions.buckets).toBeInstanceOf(Array);
expect(extensions.buckets.length).toBe(3);
expect(map(extensions.buckets, 'value')).toEqual(['html', 'php', 'gif']);
});
});
it('fails to analyze geo and attachment types', function () {
params.field = dataView.fields.getByName('point')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
params.field = dataView.fields.getByName('area')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
params.field = dataView.fields.getByName('request_body')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
});
it('fails to analyze fields that are in the mapping, but not the hits', function () {
params.field = dataView.fields.getByName('ip')!;
expect(isValidFieldDetails(fieldCalculator.getFieldValueCounts(params))).toBeFalsy();
});
it('counts the total hits', function () {
validateResults(fieldCalculator.getFieldValueCounts(params), (extensions) => {
expect(extensions.total).toBe(params.hits.length);
});
});
it('counts the hits the field exists in', function () {
params.field = dataView.fields.getByName('phpmemory')!;
validateResults(fieldCalculator.getFieldValueCounts(params), (extensions) => {
expect(extensions.exists).toBe(5);
});
});
});
});

View file

@ -1,138 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 { map, sortBy, without, each, defaults, isObject } from 'lodash';
import { i18n } from '@kbn/i18n';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
import type { DataTableRecord } from '../../../../../types';
import { Bucket, FieldDetails } from './types';
export interface FieldCountsParams {
hits: DataTableRecord[];
field: DataViewField;
dataView: DataView;
count?: number;
grouped?: boolean;
}
interface FieldCountsBucket {
count: number;
value: string;
}
const getFieldValues = (hits: DataTableRecord[], field: DataViewField): unknown[] =>
map(hits, (hit) => hit.flattened[field.name]);
const getFieldValueCounts = (params: FieldCountsParams): FieldDetails => {
params = defaults(params, {
count: 5,
grouped: false,
});
if (
params.field.type === 'geo_point' ||
params.field.type === 'geo_shape' ||
params.field.type === 'attachment'
) {
return {
error: i18n.translate(
'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage',
{
defaultMessage: 'Analysis is not available for geo fields.',
}
),
};
}
const allValues = getFieldValues(params.hits, params.field);
const missing = _countMissing(allValues);
try {
const groups = _groupValues(allValues, params);
const counts: Bucket[] = sortBy(groups, 'count')
.reverse()
.slice(0, params.count)
.map((bucket: FieldCountsBucket) => ({
value: bucket.value,
count: bucket.count as number,
percent: Number(((bucket.count / (params.hits.length - missing)) * 100).toFixed(1)),
display: params.dataView.getFormatterForField(params.field).convert(bucket.value),
}));
if (params.hits.length - missing === 0) {
return {
error: i18n.translate(
'discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage',
{
defaultMessage:
'This field is present in your Elasticsearch mapping but not in the {hitsLength} documents shown in the doc table. You may still be able to visualize or search on it.',
values: {
hitsLength: params.hits.length,
},
}
),
};
}
return {
total: params.hits.length,
exists: params.hits.length - missing,
missing,
buckets: counts,
};
} catch (e) {
return { error: e.message };
}
};
// returns a count of fields in the array that are undefined or null
const _countMissing = (array: unknown[]) => array.length - without(array, undefined, null).length;
const _groupValues = (allValues: unknown[], params: FieldCountsParams) => {
const groups: Record<string, FieldCountsBucket> = {};
let k;
allValues.forEach((value: unknown) => {
if (isObject(value) && !Array.isArray(value)) {
throw new Error(
i18n.translate(
'discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage',
{
defaultMessage: 'Analysis is not available for object fields.',
}
)
);
}
if (Array.isArray(value) && !params.grouped) {
k = value;
} else {
k = value == null ? undefined : [value];
}
each(k, (key: string) => {
if (groups.hasOwnProperty(key)) {
(groups[key] as FieldCountsBucket).count++;
} else {
groups[key] = {
value: params.grouped ? (value as string) : key,
count: 1,
};
}
});
});
return groups;
};
export const fieldCalculator = {
_groupValues,
_countMissing,
getFieldValues,
getFieldValueCounts,
};

View file

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { fieldCalculator } from './field_calculator';
import { DataTableRecord } from '../../../../../types';
import { ErrorFieldDetails, FieldDetails, ValidFieldDetails } from './types';
export const isValidFieldDetails = (details: FieldDetails): details is ValidFieldDetails =>
!(details as ErrorFieldDetails).error;
export function getDetails(
field: DataViewField,
hits: DataTableRecord[] | undefined,
dataView: DataView
) {
if (!hits) {
return undefined;
}
return fieldCalculator.getFieldValueCounts({
hits,
field,
count: 5,
grouped: false,
dataView,
});
}

View file

@ -1,22 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 React from 'react';
import { EuiProgress } from '@elastic/eui';
interface Props {
percent: number;
count: number;
value: string;
}
export function StringFieldProgressBar({ value, percent, count }: Props) {
const ariaLabel = `${value}: ${count} (${percent}%)`;
return <EuiProgress value={percent} max={100} color="success" aria-label={ariaLabel} size="s" />;
}

View file

@ -1,27 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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.
*/
export interface ValidFieldDetails {
exists: number;
total: number;
missing: number;
buckets: Bucket[];
}
export interface ErrorFieldDetails {
error: string;
}
export type FieldDetails = ValidFieldDetails | ErrorFieldDetails;
export interface Bucket {
display: string;
value: string;
percent: number;
count: number;
}

View file

@ -9,7 +9,6 @@
import { act } from 'react-dom/test-utils';
import { EuiButtonIcon, EuiPopover, EuiProgress } from '@elastic/eui';
import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { findTestSubject } from '@elastic/eui/lib/test';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { DiscoverField, DiscoverFieldProps } from './discover_field';
@ -18,15 +17,9 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { stubDataView } from '@kbn/data-views-plugin/common/data_view.stub';
import { DiscoverAppStateProvider } from '../../services/discover_app_state_container';
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
import { FetchStatus } from '../../../types';
import { DataDocuments$ } from '../../services/discover_data_state_container';
import { getDataTableRecords } from '../../../../__fixtures__/real_hits';
import * as DetailsUtil from './deprecated_stats/get_details';
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
import { FieldItemButton } from '@kbn/unified-field-list-plugin/public';
jest.spyOn(DetailsUtil, 'getDetails');
jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({
loadFieldStats: jest.fn().mockResolvedValue({
totalDocuments: 1624,
@ -57,16 +50,12 @@ jest.mock('../../../../kibana_services', () => ({
async function getComponent({
selected = false,
showFieldStats = false,
field,
onAddFilterExists = true,
showLegacyFieldTopValues = false,
}: {
selected?: boolean;
showFieldStats?: boolean;
field?: DataViewField;
onAddFilterExists?: boolean;
showLegacyFieldTopValues?: boolean;
}) {
const finalField =
field ??
@ -83,21 +72,13 @@ async function getComponent({
const dataView = stubDataView;
dataView.toSpec = () => ({});
const hits = getDataTableRecords(dataView);
const documents$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: hits,
}) as DataDocuments$;
const props: DiscoverFieldProps = {
documents$,
dataView: stubDataView,
field: finalField,
...(onAddFilterExists && { onAddFilter: jest.fn() }),
onAddField: jest.fn(),
onEditField: jest.fn(),
onRemoveField: jest.fn(),
showFieldStats,
isSelected: selected,
isEmpty: false,
groupIndex: 1,
@ -116,9 +97,6 @@ async function getComponent({
if (key === 'fields:popularLimit') {
return 5;
}
if (key === 'discover:showLegacyFieldTopValues') {
return showLegacyFieldTopValues;
}
},
},
};
@ -141,10 +119,6 @@ async function getComponent({
}
describe('discover sidebar field', function () {
beforeEach(() => {
(DetailsUtil.getDetails as jest.Mock).mockClear();
});
it('should allow selecting fields', async function () {
const { comp, props } = await getComponent({});
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
@ -155,33 +129,6 @@ describe('discover sidebar field', function () {
findTestSubject(comp, 'fieldToggle-bytes').simulate('click');
expect(props.onRemoveField).toHaveBeenCalledWith('bytes');
});
it('should trigger getDetails for showing the deprecated field stats', async function () {
const { comp, props } = await getComponent({
selected: true,
showFieldStats: true,
showLegacyFieldTopValues: true,
});
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
expect(DetailsUtil.getDetails).toHaveBeenCalledTimes(1);
expect(findTestSubject(comp, `discoverFieldDetails-${props.field.name}`).exists()).toBeTruthy();
});
it('should not allow clicking on _source', async function () {
const field = new DataViewField({
name: '_source',
type: '_source',
esTypes: ['_source'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
});
const { comp } = await getComponent({
selected: true,
field,
showLegacyFieldTopValues: true,
});
findTestSubject(comp, 'field-_source-showDetails').simulate('click');
expect(DetailsUtil.getDetails).not.toHaveBeenCalledWith();
});
it('displays warning for conflicting fields', async function () {
const field = new DataViewField({
name: 'troubled_field',
@ -198,18 +145,6 @@ describe('discover sidebar field', function () {
const dscField = findTestSubject(comp, 'field-troubled_field-showDetails');
expect(dscField.find('.kbnFieldButton__infoIcon').length).toEqual(1);
});
it('should not execute getDetails when rendered, since it can be expensive', async function () {
await getComponent({});
expect(DetailsUtil.getDetails).toHaveBeenCalledTimes(0);
});
it('should execute getDetails when show details is requested', async function () {
const { comp } = await getComponent({
showFieldStats: true,
showLegacyFieldTopValues: true,
});
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
expect(DetailsUtil.getDetails).toHaveBeenCalledTimes(1);
});
it('should not enable the popover if onAddFilter is not provided', async function () {
const field = new DataViewField({
name: '_source',
@ -236,7 +171,7 @@ describe('discover sidebar field', function () {
searchable: true,
});
const { comp } = await getComponent({ showFieldStats: true, field, onAddFilterExists: true });
const { comp } = await getComponent({ field, onAddFilterExists: true });
await act(async () => {
const fieldItem = findTestSubject(comp, 'field-machine.os.raw-showDetails');

View file

@ -23,11 +23,8 @@ import {
} from '@kbn/unified-field-list-plugin/public';
import { DragDrop } from '@kbn/dom-drag-drop';
import { DiscoverFieldStats } from './discover_field_stats';
import { DiscoverFieldDetails } from './deprecated_stats/discover_field_details';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { PLUGIN_ID, SHOW_LEGACY_FIELD_TOP_VALUES } from '../../../../../common';
import { PLUGIN_ID } from '../../../../../common';
import { getUiActions } from '../../../../kibana_services';
import { type DataDocuments$ } from '../../services/discover_data_state_container';
interface GetCommonFieldItemButtonPropsParams {
field: DataViewField;
@ -111,10 +108,6 @@ const MultiFields: React.FC<MultiFieldsProps> = memo(
);
export interface DiscoverFieldProps {
/**
* hits fetched from ES, displayed in the doc table
*/
documents$: DataDocuments$;
/**
* Determines whether add/remove button is displayed not only when focused
*/
@ -169,10 +162,6 @@ export interface DiscoverFieldProps {
*/
onDeleteField?: (fieldName: string) => void;
/**
* Optionally show or hide field stats in the popover
*/
showFieldStats?: boolean;
/**
* Columns
*/
@ -195,7 +184,6 @@ export interface DiscoverFieldProps {
}
function DiscoverFieldComponent({
documents$,
alwaysShowActionButton = false,
field,
highlight,
@ -209,12 +197,10 @@ function DiscoverFieldComponent({
multiFields,
onEditField,
onDeleteField,
showFieldStats,
contextualFields,
groupIndex,
itemIndex,
}: DiscoverFieldProps) {
const services = useDiscoverServices();
const [infoIsOpen, setOpen] = useState(false);
const isDocumentRecord = !!onAddFilter;
@ -272,33 +258,18 @@ function DiscoverFieldComponent({
);
const renderPopover = () => {
const showLegacyFieldStats = services.uiSettings.get(SHOW_LEGACY_FIELD_TOP_VALUES);
return (
<>
{showLegacyFieldStats ? ( // TODO: Deprecate and remove after ~v8.7
<>
{showFieldStats && (
<DiscoverFieldDetails
documents$={documents$}
dataView={dataView}
field={field}
onAddFilter={onAddFilter}
/>
)}
</>
) : (
<DiscoverFieldStats
field={field}
multiFields={multiFields}
dataView={dataView}
onAddFilter={addFilterAndClosePopover}
/>
)}
<DiscoverFieldStats
field={field}
multiFields={multiFields}
dataView={dataView}
onAddFilter={addFilterAndClosePopover}
/>
{multiFields && (
<>
{(showFieldStats || !showLegacyFieldStats) && <EuiSpacer size="m" />}
<EuiSpacer size="m" />
<MultiFields
multiFields={multiFields}
alwaysShowActionButton={alwaysShowActionButton}

View file

@ -101,7 +101,6 @@ export function DiscoverSidebarComponent({
isProcessing,
alwaysShowActionButtons = false,
columns,
documents$,
allFields,
onAddField,
onAddFilter,
@ -125,7 +124,6 @@ export function DiscoverSidebarComponent({
(state) => getRawRecordType(state.query) === RecordRawType.PLAIN
);
const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]);
const [selectedFieldsState, setSelectedFieldsState] = useState<SelectedFieldsResult>(
INITIAL_SELECTED_FIELDS_RESULT
);
@ -233,12 +231,10 @@ export function DiscoverSidebarComponent({
onAddField={onAddField}
onRemoveField={onRemoveField}
onAddFilter={onAddFilter}
documents$={documents$}
trackUiMetric={trackUiMetric}
multiFields={multiFieldsMap?.get(field.name)} // ideally we better calculate multifields when they are requested first from the popover
onEditField={editField}
onDeleteField={deleteField}
showFieldStats={showFieldStats}
contextualFields={columns}
groupIndex={groupIndex}
itemIndex={itemIndex}
@ -256,12 +252,10 @@ export function DiscoverSidebarComponent({
onAddField,
onRemoveField,
onAddFilter,
documents$,
trackUiMetric,
multiFieldsMap,
editField,
deleteField,
showFieldStats,
columns,
selectedFieldsState.selectedFieldsMap,
]

View file

@ -30,7 +30,6 @@ import {
TRUNCATE_MAX_HEIGHT,
SHOW_FIELD_STATISTICS,
ROW_HEIGHT_OPTION,
SHOW_LEGACY_FIELD_TOP_VALUES,
ENABLE_SQL,
} from '../common';
import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../common/constants';
@ -125,25 +124,6 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record<string, U
category: ['discover'],
schema: schema.boolean(),
},
[SHOW_LEGACY_FIELD_TOP_VALUES]: {
name: i18n.translate('discover.advancedSettings.showLegacyFieldStatsTitle', {
defaultMessage: 'Top values calculation',
}),
value: false,
type: 'boolean',
description: i18n.translate('discover.advancedSettings.showLegacyFieldStatsText', {
defaultMessage:
'To calculate the top values for a field in the sidebar using 500 instead of 5,000 records per shard, turn on this option.',
}),
category: ['discover'],
schema: schema.boolean(),
deprecation: {
message: i18n.translate('discover.advancedSettings.showLegacyFieldStatsTextDeprecation', {
defaultMessage: 'This setting is deprecated and will not be supported in a future version.',
}),
docLinksKey: 'discoverSettings',
},
},
[DOC_HIDE_TIME_COLUMN_SETTING]: {
name: i18n.translate('discover.advancedSettings.docTableHideTimeColumnTitle', {
defaultMessage: "Hide 'Time' column",

View file

@ -167,10 +167,6 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'discover:showLegacyFieldTopValues': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'discover:sampleSize': {
type: 'long',
_meta: { description: 'Non-default value of setting.' },

View file

@ -80,7 +80,6 @@ export interface UsageStats {
'doc_table:hideTimeColumn': boolean;
'discover:sampleSize': number;
'discover:sampleRowsPerPage': number;
'discover:showLegacyFieldTopValues': boolean;
defaultColumns: string[];
'context:defaultSize': number;
'context:tieBreakerFields': string[];

View file

@ -8636,12 +8636,6 @@
"description": "Non-default value of setting."
}
},
"discover:showLegacyFieldTopValues": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"discover:sampleSize": {
"type": "long",
"_meta": {

View file

@ -32,6 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const filterBar = getService('filterBar');
const docTable = getService('docTable');
const PageObjects = getPageObjects([
'common',
'header',
@ -50,7 +51,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.uiSettings.replace({});
await kibanaServer.uiSettings.update({
'doc_table:legacy': true,
'discover:showLegacyFieldTopValues': true,
});
});
@ -437,12 +437,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should filter by scripted field value in Discover', async function () {
await PageObjects.discover.clickFieldListItem(scriptedPainlessFieldName2);
await log.debug('filter by "Sep 17, 2015 @ 23:00" in the expanded scripted field list');
await PageObjects.discover.clickFieldListPlusFilter(
scriptedPainlessFieldName2,
'1442531297065'
);
await PageObjects.header.waitUntilLoadingHasFinished();
await docTable.toggleRowExpanded();
const firstRow = await docTable.getDetailsRow();
await docTable.addInclusiveFilter(firstRow, scriptedPainlessFieldName2);
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async function () {

View file

@ -2142,11 +2142,6 @@
"discover.docTable.totalDocuments": "{totalDocuments} documents",
"discover.dscTour.stepAddFields.description": "Cliquez sur {plusIcon} pour ajouter les champs qui vous intéressent.",
"discover.dscTour.stepExpand.description": "Cliquez sur {expandIcon} pour afficher, comparer et filtrer les documents.",
"discover.fieldChooser.detailViews.existsInRecordsText": "Existe dans {value} / {totalValue} enregistrements",
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "Exclure {field} : \"{value}\"",
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "Filtrer sur {field} : \"{value}\"",
"discover.fieldChooser.detailViews.valueOfRecordsText": "{value} / {totalValue} enregistrements",
"discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "Ce champ est présent dans votre mapping Elasticsearch, mais pas dans les {hitsLength} documents affichés dans le tableau des documents. Cependant, vous pouvez toujours le consulter ou effectuer une recherche dessus.",
"discover.grid.copyClipboardButtonTitle": "Copier la valeur de {column}",
"discover.grid.copyColumnValuesToClipboard.toastTitle": "Valeurs de la colonne \"{column}\" copiées dans le presse-papiers",
"discover.grid.filterForAria": "Filtrer sur cette {value}",
@ -2204,8 +2199,6 @@
"discover.advancedSettings.sampleSizeTitle": "Lignes max. par tableau",
"discover.advancedSettings.searchOnPageLoadText": "Détermine si une recherche est exécutée lors du premier chargement de Discover. Ce paramètre n'a pas d'effet lors du chargement dune recherche enregistrée.",
"discover.advancedSettings.searchOnPageLoadTitle": "Recherche au chargement de la page",
"discover.advancedSettings.showLegacyFieldStatsText": "Pour calculer les valeurs les plus élevées d'un champ dans la barre latérale en utilisant 500 au lieu de 5 000 enregistrements par partition, activez cette option.",
"discover.advancedSettings.showLegacyFieldStatsTitle": "Calcul des valeurs les plus élevées",
"discover.advancedSettings.sortDefaultOrderText": "Détermine le sens de tri par défaut pour les vues de données temporelles dans l'application Discover.",
"discover.advancedSettings.sortDefaultOrderTitle": "Sens de tri par défaut",
"discover.advancedSettings.sortOrderAsc": "Croissant",
@ -2330,18 +2323,14 @@
"discover.embeddable.search.displayName": "rechercher",
"discover.fieldChooser.addField.label": "Ajouter un champ",
"discover.fieldChooser.availableFieldsTooltip": "Champs disponibles pour l'affichage dans le tableau.",
"discover.fieldChooser.detailViews.emptyStringText": "Chaîne vide",
"discover.fieldChooser.discoverField.actions": "Actions",
"discover.fieldChooser.discoverField.addFieldTooltip": "Ajouter le champ en tant que colonne",
"discover.fieldChooser.discoverField.fieldTopValuesLabel": "Top 5 des valeurs",
"discover.fieldChooser.discoverField.multiField": "champ multiple",
"discover.fieldChooser.discoverField.multiFields": "Champs multiples",
"discover.fieldChooser.discoverField.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.",
"discover.fieldChooser.discoverField.name": "Champ",
"discover.fieldChooser.discoverField.removeFieldTooltip": "Supprimer le champ du tableau",
"discover.fieldChooser.discoverField.value": "Valeur",
"discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "L'analyse n'est pas disponible pour les champs géométriques.",
"discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "L'analyse n'est pas disponible pour les champs d'objet.",
"discover.fieldChooser.fieldsMobileButtonLabel": "Champs",
"discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "Index et champs",
"discover.fieldList.flyoutBackIcon": "Retour",

View file

@ -2142,11 +2142,6 @@
"discover.docTable.totalDocuments": "{totalDocuments}ドキュメント",
"discover.dscTour.stepAddFields.description": "{plusIcon}をクリックして、関心があるフィールドを追加します。",
"discover.dscTour.stepExpand.description": "{expandIcon}をクリックすると、ドキュメントを表示、比較、フィルタリングできます。",
"discover.fieldChooser.detailViews.existsInRecordsText": "{value} / {totalValue}レコードに存在します",
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"",
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}のフィルター:\"{value}\"",
"discover.fieldChooser.detailViews.valueOfRecordsText": "{value} / {totalValue}レコード",
"discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "このフィールドはElasticsearchマッピングに表示されますが、ドキュメントテーブルの{hitsLength}件のドキュメントには含まれません。可視化や検索は可能な場合があります。",
"discover.grid.copyClipboardButtonTitle": "{column}の値をコピー",
"discover.grid.copyColumnValuesToClipboard.toastTitle": "\"{column}\"列の値がクリップボードにコピーされました",
"discover.grid.filterForAria": "この{value}でフィルターを適用",
@ -2204,8 +2199,6 @@
"discover.advancedSettings.sampleSizeTitle": "テーブルごとの最大行数",
"discover.advancedSettings.searchOnPageLoadText": "Discover の最初の読み込み時に検索を実行するかを制御します。この設定は、保存された検索の読み込み時には影響しません。",
"discover.advancedSettings.searchOnPageLoadTitle": "ページの読み込み時の検索",
"discover.advancedSettings.showLegacyFieldStatsText": "シャードごとに5,000レコードではなく、500レコードを使用して、サイドバーのフィールドの上位の値を計算するには、このオプションをオンにします。",
"discover.advancedSettings.showLegacyFieldStatsTitle": "上位の値の計算",
"discover.advancedSettings.sortDefaultOrderText": "Discover アプリのデータビューに基づく時刻のデフォルトの並べ替え方向をコントロールします。",
"discover.advancedSettings.sortDefaultOrderTitle": "デフォルトの並べ替え方向",
"discover.advancedSettings.sortOrderAsc": "昇順",
@ -2330,18 +2323,14 @@
"discover.embeddable.search.displayName": "検索",
"discover.fieldChooser.addField.label": "フィールドを追加",
"discover.fieldChooser.availableFieldsTooltip": "フィールドをテーブルに表示できます。",
"discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
"discover.fieldChooser.discoverField.actions": "アクション",
"discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加",
"discover.fieldChooser.discoverField.fieldTopValuesLabel": "トップ5の値",
"discover.fieldChooser.discoverField.multiField": "複数フィールド",
"discover.fieldChooser.discoverField.multiFields": "マルチフィールド",
"discover.fieldChooser.discoverField.multiFieldTooltipContent": "複数フィールドにはフィールドごとに複数の値を入力できます",
"discover.fieldChooser.discoverField.name": "フィールド",
"discover.fieldChooser.discoverField.removeFieldTooltip": "フィールドを表から削除",
"discover.fieldChooser.discoverField.value": "値",
"discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "ジオフィールドは分析できません。",
"discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "オブジェクトフィールドは分析できません。",
"discover.fieldChooser.fieldsMobileButtonLabel": "フィールド",
"discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "インデックスとフィールド",
"discover.fieldList.flyoutBackIcon": "戻る",

View file

@ -2142,11 +2142,6 @@
"discover.docTable.totalDocuments": "{totalDocuments} 个文档",
"discover.dscTour.stepAddFields.description": "单击 {plusIcon} 以添加您感兴趣的字段。",
"discover.dscTour.stepExpand.description": "单击 {expandIcon} 以查看、比较和筛选文档。",
"discover.fieldChooser.detailViews.existsInRecordsText": "存在于 {value}/{totalValue} 条记录中",
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}“{value}”",
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}“{value}”",
"discover.fieldChooser.detailViews.valueOfRecordsText": "{value}/{totalValue} 条记录",
"discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "此字段在您的 Elasticsearch 映射中,但不在文档表中显示的 {hitsLength} 个文档中。您可能仍能够基于它可视化或搜索。",
"discover.grid.copyClipboardButtonTitle": "复制 {column} 的值",
"discover.grid.copyColumnValuesToClipboard.toastTitle": "“{column}”列的值已复制到剪贴板",
"discover.grid.filterForAria": "筛留此 {value}",
@ -2204,8 +2199,6 @@
"discover.advancedSettings.sampleSizeTitle": "每个表的最大行数",
"discover.advancedSettings.searchOnPageLoadText": "控制在 Discover 首次加载时是否执行搜索。加载已保存搜索时,此设置无效。",
"discover.advancedSettings.searchOnPageLoadTitle": "在页面加载时搜索",
"discover.advancedSettings.showLegacyFieldStatsText": "要在侧边栏中每分片使用 500 条而不是 5,000 条记录计算字段的排名最前值,请打开此选项。",
"discover.advancedSettings.showLegacyFieldStatsTitle": "排名最前值计算",
"discover.advancedSettings.sortDefaultOrderText": "在 Discover 应用中控制基于时间的数据视图的默认排序方向。",
"discover.advancedSettings.sortDefaultOrderTitle": "默认排序方向",
"discover.advancedSettings.sortOrderAsc": "升序",
@ -2330,18 +2323,14 @@
"discover.embeddable.search.displayName": "搜索",
"discover.fieldChooser.addField.label": "添加字段",
"discover.fieldChooser.availableFieldsTooltip": "适用于在表中显示的字段。",
"discover.fieldChooser.detailViews.emptyStringText": "空字符串",
"discover.fieldChooser.discoverField.actions": "操作",
"discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列",
"discover.fieldChooser.discoverField.fieldTopValuesLabel": "排名前 5 值",
"discover.fieldChooser.discoverField.multiField": "多字段",
"discover.fieldChooser.discoverField.multiFields": "多字段",
"discover.fieldChooser.discoverField.multiFieldTooltipContent": "多字段的每个字段可以有多个值",
"discover.fieldChooser.discoverField.name": "字段",
"discover.fieldChooser.discoverField.removeFieldTooltip": "从表中移除字段",
"discover.fieldChooser.discoverField.value": "值",
"discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForGeoFieldsErrorMessage": "分析不适用于地理字段。",
"discover.fieldChooser.fieldCalculator.analysisIsNotAvailableForObjectFieldsErrorMessage": "分析不适用于对象字段。",
"discover.fieldChooser.fieldsMobileButtonLabel": "字段",
"discover.fieldChooser.filter.indexAndFieldsSectionAriaLabel": "索引和字段",
"discover.fieldList.flyoutBackIcon": "返回",