[8.x] [Discover] Remove field popover stats for ES|QL mode (#198948) (#199472)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Discover] Remove field popover stats for ES|QL mode
(#198948)](https://github.com/elastic/kibana/pull/198948)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Julia
Rechkunova","email":"julia.rechkunova@elastic.co"},"sourceCommit":{"committedDate":"2024-11-08T11:04:47Z","message":"[Discover]
Remove field popover stats for ES|QL mode (#198948)\n\n- Related to
https://github.com/elastic/kibana/pull/197538\r\n\r\n##
Summary\r\n\r\nThis PR removes the support of showing stats in the field
popover in\r\nES|QL mode as this UX will be revisited in the future to
provide better\r\nresults.\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"e883ac5470352196252e300454e72b6d53696bda","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:DataDiscovery","backport:prev-minor","Feature:UnifiedFieldList","Feature:ES|QL","Project:OneDiscover"],"number":198948,"url":"https://github.com/elastic/kibana/pull/198948","mergeCommit":{"message":"[Discover]
Remove field popover stats for ES|QL mode (#198948)\n\n- Related to
https://github.com/elastic/kibana/pull/197538\r\n\r\n##
Summary\r\n\r\nThis PR removes the support of showing stats in the field
popover in\r\nES|QL mode as this UX will be revisited in the future to
provide better\r\nresults.\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"e883ac5470352196252e300454e72b6d53696bda"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/198948","number":198948,"mergeCommit":{"message":"[Discover]
Remove field popover stats for ES|QL mode (#198948)\n\n- Related to
https://github.com/elastic/kibana/pull/197538\r\n\r\n##
Summary\r\n\r\nThis PR removes the support of showing stats in the field
popover in\r\nES|QL mode as this UX will be revisited in the future to
provide better\r\nresults.\r\n\r\n\r\n### Checklist\r\n\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"e883ac5470352196252e300454e72b6d53696bda"}}]}]
BACKPORT-->
This commit is contained in:
Julia Rechkunova 2024-11-08 15:58:12 +01:00 committed by GitHub
parent f132d6bb49
commit 782b693956
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 82 additions and 776 deletions

View file

@ -830,4 +830,14 @@ describe('UnifiedFieldList FieldStats', () => {
expect(wrapper.text()).toBe('Summarymin29674max36821994Calculated from 5000 sample records.');
});
it('should not request field stats for ES|QL query', async () => {
const wrapper = await mountComponent(
<FieldStats {...defaultProps} query={{ esql: 'from logs* | limit 10' }} />
);
expect(loadFieldStats).toHaveBeenCalledTimes(0);
expect(wrapper.text()).toBe('Analysis is not available for this field.');
});
});

View file

@ -42,7 +42,6 @@ import {
canProvideNumberSummaryForField,
} from '../../utils/can_provide_stats';
import { loadFieldStats } from '../../services/field_stats';
import { loadFieldStatsTextBased } from '../../services/field_stats_text_based';
import type { AddFieldFilterHandler } from '../../types';
import {
FieldTopValues,
@ -136,7 +135,7 @@ const FieldStatsComponent: React.FC<FieldStatsProps> = ({
const [dataView, changeDataView] = useState<DataView | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const isCanceledRef = useRef<boolean>(false);
const isTextBased = !!query && isOfAggregateQueryType(query);
const isEsqlQuery = !!query && isOfAggregateQueryType(query);
const setState: typeof changeState = useCallback(
(nextState) => {
@ -178,6 +177,12 @@ const FieldStatsComponent: React.FC<FieldStatsProps> = ({
setDataView(loadedDataView);
if (isEsqlQuery) {
// Not supported yet for ES|QL queries
// Previous implementation was removed in https://github.com/elastic/kibana/pull/198948/
return;
}
if (state.isLoading) {
return;
}
@ -187,32 +192,17 @@ const FieldStatsComponent: React.FC<FieldStatsProps> = ({
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
const results = isTextBased
? await loadFieldStatsTextBased({
services: { data },
dataView: loadedDataView,
field,
fromDate,
toDate,
baseQuery: query,
abortController: abortControllerRef.current,
})
: await loadFieldStats({
services: { data },
dataView: loadedDataView,
field,
fromDate,
toDate,
dslQuery:
dslQuery ??
buildEsQuery(
loadedDataView,
query ?? [],
filters ?? [],
getEsQueryConfig(uiSettings)
),
abortController: abortControllerRef.current,
});
const results = await loadFieldStats({
services: { data },
dataView: loadedDataView,
field,
fromDate,
toDate,
dslQuery:
dslQuery ??
buildEsQuery(loadedDataView, query ?? [], filters ?? [], getEsQueryConfig(uiSettings)),
abortController: abortControllerRef.current,
});
abortControllerRef.current = null;
@ -297,7 +287,7 @@ const FieldStatsComponent: React.FC<FieldStatsProps> = ({
let title = <></>;
function combineWithTitleAndFooter(el: React.ReactElement) {
const countsElement = getCountsElement(state, services, isTextBased, dataTestSubject);
const countsElement = getCountsElement(state, services, isEsqlQuery, dataTestSubject);
return (
<>
@ -319,7 +309,7 @@ const FieldStatsComponent: React.FC<FieldStatsProps> = ({
);
}
if (!canProvideStatsForField(field, isTextBased)) {
if (!canProvideStatsForField(field, isEsqlQuery)) {
const messageNoAnalysis = (
<FieldSummaryMessage
message={i18n.translate('unifiedFieldList.fieldStats.notAvailableForThisFieldDescription', {
@ -336,7 +326,7 @@ const FieldStatsComponent: React.FC<FieldStatsProps> = ({
: messageNoAnalysis;
}
if (canProvideNumberSummaryForField(field, isTextBased) && isNumberSummaryValid(numberSummary)) {
if (canProvideNumberSummaryForField(field, isEsqlQuery) && isNumberSummaryValid(numberSummary)) {
title = (
<EuiTitle size="xxxs">
<h6>
@ -563,21 +553,19 @@ const FieldStatsComponent: React.FC<FieldStatsProps> = ({
function getCountsElement(
state: FieldStatsState,
services: FieldStatsServices,
isTextBased: boolean,
isEsqlQuery: boolean,
dataTestSubject: string
): JSX.Element {
const dataTestSubjDocsCount = 'unifiedFieldStats-statsFooter-docsCount';
const { fieldFormats } = services;
const { totalDocuments, sampledValues, sampledDocuments, topValues } = state;
const { totalDocuments, sampledDocuments } = state;
if (!totalDocuments) {
if (!totalDocuments || isEsqlQuery) {
return <></>;
}
let labelElement;
if (isTextBased) {
labelElement = topValues?.areExamples ? (
const labelElement =
sampledDocuments && sampledDocuments < totalDocuments ? (
<FormattedMessage
id="unifiedFieldList.fieldStats.calculatedFromSampleRecordsLabel"
defaultMessage="Calculated from {sampledDocumentsFormatted} sample {sampledDocuments, plural, one {record} other {records}}."
@ -594,54 +582,20 @@ function getCountsElement(
/>
) : (
<FormattedMessage
id="unifiedFieldList.fieldStats.calculatedFromSampleValuesLabel"
defaultMessage="Calculated from {sampledValuesFormatted} sample {sampledValues, plural, one {value} other {values}}."
id="unifiedFieldList.fieldStats.calculatedFromTotalRecordsLabel"
defaultMessage="Calculated from {totalDocumentsFormatted} {totalDocuments, plural, one {record} other {records}}."
values={{
sampledValues,
sampledValuesFormatted: (
totalDocuments,
totalDocumentsFormatted: (
<strong data-test-subj={dataTestSubjDocsCount}>
{fieldFormats
.getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER])
.convert(sampledValues)}
.convert(totalDocuments)}
</strong>
),
}}
/>
);
} else {
labelElement =
sampledDocuments && sampledDocuments < totalDocuments ? (
<FormattedMessage
id="unifiedFieldList.fieldStats.calculatedFromSampleRecordsLabel"
defaultMessage="Calculated from {sampledDocumentsFormatted} sample {sampledDocuments, plural, one {record} other {records}}."
values={{
sampledDocuments,
sampledDocumentsFormatted: (
<strong data-test-subj={dataTestSubjDocsCount}>
{fieldFormats
.getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER])
.convert(sampledDocuments)}
</strong>
),
}}
/>
) : (
<FormattedMessage
id="unifiedFieldList.fieldStats.calculatedFromTotalRecordsLabel"
defaultMessage="Calculated from {totalDocumentsFormatted} {totalDocuments, plural, one {record} other {records}}."
values={{
totalDocuments,
totalDocumentsFormatted: (
<strong data-test-subj={dataTestSubjDocsCount}>
{fieldFormats
.getDefaultInstance(KBN_FIELD_TYPES.NUMBER, [ES_FIELD_TYPES.INTEGER])
.convert(totalDocuments)}
</strong>
),
}}
/>
);
}
return (
<EuiText color="subdued" size="xs" data-test-subj={`${dataTestSubject}-statsFooter`}>

View file

@ -32,7 +32,7 @@ import type {
UnifiedFieldListSidebarContainerStateService,
AddFieldFilterHandler,
} from '../../types';
import { canProvideStatsForFieldTextBased } from '../../utils/can_provide_stats';
import { canProvideStatsForEsqlField } from '../../utils/can_provide_stats';
interface GetCommonFieldItemButtonPropsParams {
stateService: UnifiedFieldListSidebarContainerStateService;
@ -405,7 +405,7 @@ function UnifiedFieldListItemComponent({
/>
)}
renderContent={
(searchMode === 'text-based' && canProvideStatsForFieldTextBased(field)) ||
(searchMode === 'text-based' && canProvideStatsForEsqlField(field)) ||
searchMode === 'documents'
? renderPopover
: undefined

View file

@ -223,7 +223,7 @@ describe('fieldExamplesCalculator', function () {
values: getFieldValues(hits, dataView.fields.getByName('extension')!, dataView),
field: dataView.fields.getByName('extension')!,
count: 3,
isTextBased: false,
isEsqlQuery: false,
};
});
@ -286,33 +286,19 @@ describe('fieldExamplesCalculator', function () {
expect(getFieldExampleBuckets(params).sampledValues).toBe(5);
});
it('works for text-based', function () {
const result = getFieldExampleBuckets({
values: [['a'], ['b'], ['a'], ['a']],
field: { name: 'message', type: 'string', esTypes: ['text'] } as DataViewField,
isTextBased: true,
});
expect(result).toMatchInlineSnapshot(`
Object {
"buckets": Array [
Object {
"count": 3,
"key": "a",
},
Object {
"count": 1,
"key": "b",
},
],
"sampledDocuments": 4,
"sampledValues": 4,
}
`);
it('should not work for ES|QL', function () {
expect(() =>
getFieldExampleBuckets({
values: [['a'], ['b'], ['a'], ['a']],
field: { name: 'message', type: 'string', esTypes: ['text'] } as DataViewField,
isEsqlQuery: true,
})
).toThrowError();
expect(() =>
getFieldExampleBuckets({
values: [['a'], ['b'], ['a'], ['a']],
field: { name: 'message', type: 'string', esTypes: ['keyword'] } as DataViewField,
isTextBased: true,
isEsqlQuery: true,
})
).toThrowError();
});

View file

@ -23,7 +23,7 @@ export interface FieldValueCountsParams {
values: FieldHitValue[];
field: DataViewField;
count?: number;
isTextBased: boolean;
isEsqlQuery: boolean;
}
export function getFieldExampleBuckets(params: FieldValueCountsParams, formatter?: FieldFormat) {
@ -31,7 +31,7 @@ export function getFieldExampleBuckets(params: FieldValueCountsParams, formatter
count: DEFAULT_SIMPLE_EXAMPLES_SIZE,
});
if (!canProvideExamplesForField(params.field, params.isTextBased)) {
if (!canProvideExamplesForField(params.field, params.isEsqlQuery)) {
throw new Error(
`Analysis is not available this field type: "${params.field.type}". Field name: "${params.field.name}"`
);

View file

@ -416,7 +416,7 @@ export async function getSimpleExamples(
values: getFieldValues(simpleExamplesResult.hits.hits, field, dataView),
field,
count: DEFAULT_SIMPLE_EXAMPLES_SIZE,
isTextBased: false,
isEsqlQuery: false,
},
formatter
);

View file

@ -1,129 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataViewField } from '@kbn/data-views-plugin/common';
import { buildSearchFilter, fetchAndCalculateFieldStats } from './field_stats_utils_text_based';
describe('fieldStatsUtilsTextBased', function () {
describe('buildSearchFilter()', () => {
it('should create a time range filter', () => {
expect(
buildSearchFilter({
timeFieldName: 'timestamp',
fromDate: '2022-12-05T23:00:00.000Z',
toDate: '2023-01-05T09:33:05.359Z',
})
).toMatchInlineSnapshot(`
Object {
"range": Object {
"timestamp": Object {
"format": "strict_date_optional_time",
"gte": "2022-12-05T23:00:00.000Z",
"lte": "2023-01-05T09:33:05.359Z",
},
},
}
`);
});
it('should not create a time range filter', () => {
expect(
buildSearchFilter({
timeFieldName: undefined,
fromDate: '2022-12-05T23:00:00.000Z',
toDate: '2023-01-05T09:33:05.359Z',
})
).toBeNull();
});
});
describe('fetchAndCalculateFieldStats()', () => {
it('should provide top values', async () => {
const searchHandler = jest.fn().mockResolvedValue({
values: [
[3, 'a'],
[1, 'b'],
],
});
expect(
await fetchAndCalculateFieldStats({
searchHandler,
esqlBaseQuery: 'from logs* | limit 1000',
field: { name: 'message', type: 'string', esTypes: ['keyword'] } as DataViewField,
})
).toMatchInlineSnapshot(`
Object {
"sampledDocuments": 4,
"sampledValues": 4,
"topValues": Object {
"buckets": Array [
Object {
"count": 3,
"key": "a",
},
Object {
"count": 1,
"key": "b",
},
],
},
"totalDocuments": 4,
}
`);
expect(searchHandler).toHaveBeenCalledWith(
expect.objectContaining({
query:
'from logs* | limit 1000\n| WHERE `message` IS NOT NULL\n | STATS `message_terms` = count(`message`) BY `message`\n | SORT `message_terms` DESC\n | LIMIT 10',
})
);
});
it('should provide text examples', async () => {
const searchHandler = jest.fn().mockResolvedValue({
values: [[['programming', 'cool']], ['elastic', 'cool']],
});
expect(
await fetchAndCalculateFieldStats({
searchHandler,
esqlBaseQuery: 'from logs* | limit 1000',
field: { name: 'message', type: 'string', esTypes: ['text'] } as DataViewField,
})
).toMatchInlineSnapshot(`
Object {
"sampledDocuments": 2,
"sampledValues": 4,
"topValues": Object {
"areExamples": true,
"buckets": Array [
Object {
"count": 2,
"key": "cool",
},
Object {
"count": 1,
"key": "elastic",
},
Object {
"count": 1,
"key": "programming",
},
],
},
"totalDocuments": 2,
}
`);
expect(searchHandler).toHaveBeenCalledWith(
expect.objectContaining({
query:
'from logs* | limit 1000\n| WHERE `message` IS NOT NULL\n | KEEP `message`\n | LIMIT 100',
})
);
});
});
});

View file

@ -1,156 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { ESQLSearchResponse } from '@kbn/es-types';
import { appendToESQLQuery } from '@kbn/esql-utils';
import type { DataViewField } from '@kbn/data-views-plugin/common';
import type { FieldStatsResponse } from '../../types';
import {
DEFAULT_TOP_VALUES_SIZE,
DEFAULT_SIMPLE_EXAMPLES_SIZE,
SIMPLE_EXAMPLES_FETCH_SIZE,
} from '../../constants';
import {
canProvideStatsForFieldTextBased,
canProvideTopValuesForFieldTextBased,
canProvideExamplesForField,
} from '../../utils/can_provide_stats';
import { getFieldExampleBuckets } from '../field_examples_calculator';
export type SearchHandlerTextBased = ({ query }: { query: string }) => Promise<ESQLSearchResponse>;
export function buildSearchFilter({
timeFieldName,
fromDate,
toDate,
}: {
timeFieldName?: string;
fromDate: string;
toDate: string;
}) {
return timeFieldName
? {
range: {
[timeFieldName]: {
gte: fromDate,
lte: toDate,
format: 'strict_date_optional_time',
},
},
}
: null;
}
interface FetchAndCalculateFieldStatsParams {
searchHandler: SearchHandlerTextBased;
field: DataViewField;
esqlBaseQuery: string;
}
export async function fetchAndCalculateFieldStats(params: FetchAndCalculateFieldStatsParams) {
const { field } = params;
if (!canProvideStatsForFieldTextBased(field)) {
return {};
}
if (field.type === 'boolean') {
return await getStringTopValues(params, 3);
}
if (canProvideTopValuesForFieldTextBased(field)) {
return await getStringTopValues(params);
}
if (canProvideExamplesForField(field, true)) {
return await getSimpleTextExamples(params);
}
return {};
}
export async function getStringTopValues(
params: FetchAndCalculateFieldStatsParams,
size = DEFAULT_TOP_VALUES_SIZE
): Promise<FieldStatsResponse<string | boolean>> {
const { searchHandler, field, esqlBaseQuery } = params;
const safeEsqlFieldName = getSafeESQLFieldName(field.name);
const safeEsqlFieldNameTerms = getSafeESQLFieldName(`${field.name}_terms`);
const esqlQuery = appendToESQLQuery(
esqlBaseQuery,
`| WHERE ${safeEsqlFieldName} IS NOT NULL
| STATS ${safeEsqlFieldNameTerms} = count(${safeEsqlFieldName}) BY ${safeEsqlFieldName}
| SORT ${safeEsqlFieldNameTerms} DESC
| LIMIT ${size}`
);
const result = await searchHandler({ query: esqlQuery });
const values = result?.values as Array<[number, string]>;
if (!values?.length) {
return {};
}
const sampledValues = values?.reduce((acc: number, row) => acc + row[0], 0);
const topValues = {
buckets: values.map((value) => ({
count: value[0],
key: value[1],
})),
};
return {
totalDocuments: sampledValues,
sampledDocuments: sampledValues,
sampledValues,
topValues,
};
}
export async function getSimpleTextExamples(
params: FetchAndCalculateFieldStatsParams
): Promise<FieldStatsResponse<string | boolean>> {
const { searchHandler, field, esqlBaseQuery } = params;
const safeEsqlFieldName = getSafeESQLFieldName(field.name);
const esqlQuery = appendToESQLQuery(
esqlBaseQuery,
`| WHERE ${safeEsqlFieldName} IS NOT NULL
| KEEP ${safeEsqlFieldName}
| LIMIT ${SIMPLE_EXAMPLES_FETCH_SIZE}`
);
const result = await searchHandler({ query: esqlQuery });
const values = ((result?.values as Array<[string | string[]]>) || []).map((value) =>
Array.isArray(value) && value.length === 1 ? value[0] : value
);
if (!values?.length) {
return {};
}
const sampledDocuments = values?.length;
const fieldExampleBuckets = getFieldExampleBuckets({
values,
field,
count: DEFAULT_SIMPLE_EXAMPLES_SIZE,
isTextBased: true,
});
return {
totalDocuments: sampledDocuments,
sampledDocuments: fieldExampleBuckets.sampledDocuments,
sampledValues: fieldExampleBuckets.sampledValues,
topValues: {
buckets: fieldExampleBuckets.buckets,
areExamples: true,
},
};
}
function getSafeESQLFieldName(str: string): string {
return `\`${str}\``;
}

View file

@ -1,10 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { loadFieldStatsTextBased } from './load_field_stats_text_based';

View file

@ -1,89 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import { getESQLWithSafeLimit, getESQLResults } from '@kbn/esql-utils';
import type { FieldStatsResponse } from '../../types';
import {
buildSearchFilter,
SearchHandlerTextBased,
fetchAndCalculateFieldStats,
} from './field_stats_utils_text_based';
import { ESQL_SAFE_LIMIT } from '../../constants';
interface FetchFieldStatsParamsTextBased {
services: {
data: DataPublicPluginStart;
};
dataView: DataView;
field: DataViewField;
fromDate: string;
toDate: string;
baseQuery: AggregateQuery;
abortController?: AbortController;
}
export type LoadFieldStatsTextBasedHandler = (
params: FetchFieldStatsParamsTextBased
) => Promise<FieldStatsResponse<string | boolean>>;
/**
* Loads and aggregates stats data for an ES|QL query field
* @param services
* @param dataView
* @param field
* @param fromDate
* @param toDate
* @param baseQuery
* @param abortController
*/
export const loadFieldStatsTextBased: LoadFieldStatsTextBasedHandler = async ({
services,
dataView,
field,
fromDate,
toDate,
baseQuery,
abortController,
}) => {
const { data } = services;
try {
if (!dataView?.id || !field?.type) {
return {};
}
const searchHandler: SearchHandlerTextBased = async ({ query }) => {
const filter = buildSearchFilter({ timeFieldName: dataView.timeFieldName, fromDate, toDate });
const result = await getESQLResults({
esqlQuery: query,
filter,
search: data.search.search,
signal: abortController?.signal,
timeRange: { from: fromDate, to: toDate },
});
return result.response;
};
if (!('esql' in baseQuery)) {
throw new Error('query must be of type AggregateQuery');
}
return await fetchAndCalculateFieldStats({
searchHandler,
field,
esqlBaseQuery: getESQLWithSafeLimit(baseQuery.esql, ESQL_SAFE_LIMIT),
});
} catch (error) {
// console.error(error);
throw new Error('Could not provide field stats', { cause: error });
}
};

View file

@ -10,7 +10,7 @@
import {
canProvideStatsForField,
canProvideExamplesForField,
canProvideStatsForFieldTextBased,
canProvideStatsForEsqlField,
} from './can_provide_stats';
import type { DataViewField } from '@kbn/data-views-plugin/common';
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
@ -34,40 +34,12 @@ describe('can_provide_stats', function () {
);
});
it('works for text based columns', function () {
it('should not work for ES|QL columns', function () {
expect(
canProvideStatsForField(
{ name: 'message', type: 'string', esTypes: ['text'] } as DataViewField,
true
)
).toBe(true);
expect(
canProvideStatsForField(
{ name: 'message', type: 'string', esTypes: ['keyword'] } as DataViewField,
true
)
).toBe(true);
expect(
canProvideStatsForField({ name: 'message', type: 'number' } as DataViewField, true)
).toBe(true);
expect(
canProvideStatsForField({ name: 'message', type: 'boolean' } as DataViewField, true)
).toBe(true);
expect(canProvideStatsForField({ name: 'message', type: 'ip' } as DataViewField, true)).toBe(
true
);
expect(
canProvideStatsForField({ name: 'message', type: 'geo_point' } as DataViewField, true)
).toBe(true);
expect(
canProvideStatsForField(
{ name: '_id', type: 'string', esTypes: ['keyword'] } as DataViewField,
true
)
).toBe(true);
expect(
canProvideStatsForField({ name: 'message', type: 'date' } as DataViewField, true)
).toBe(false);
});
});
@ -82,83 +54,24 @@ describe('can_provide_stats', function () {
);
});
it('works for text based columns', function () {
it('should not work for ES|QL columns', function () {
expect(
canProvideExamplesForField(
{ name: 'message', type: 'string', esTypes: ['text'] } as DataViewField,
true
)
).toBe(true);
expect(
canProvideExamplesForField(
{ name: 'message', type: 'string', esTypes: ['keyword'] } as DataViewField,
true
)
).toBe(false);
expect(
canProvideExamplesForField({ name: 'message', type: 'number' } as DataViewField, true)
).toBe(false);
expect(
canProvideExamplesForField({ name: 'message', type: 'boolean' } as DataViewField, true)
).toBe(false);
expect(
canProvideExamplesForField({ name: 'message', type: 'ip' } as DataViewField, true)
).toBe(false);
expect(
canProvideExamplesForField({ name: 'message', type: 'geo_point' } as DataViewField, true)
).toBe(true);
expect(
canProvideExamplesForField({ name: 'message', type: 'date' } as DataViewField, true)
).toBe(false);
expect(
canProvideStatsForField(
{ name: '_id', type: 'string', esTypes: ['keyword'] } as DataViewField,
true
)
).toBe(true);
});
describe('canProvideStatsForFieldTextBased', function () {
it('works for text based columns', function () {
describe('canProvideStatsForEsqlField', function () {
it('should not work for ES|QL columns', function () {
expect(
canProvideStatsForFieldTextBased({
canProvideStatsForEsqlField({
name: 'message',
type: 'string',
esTypes: ['text'],
} as DataViewField)
).toBe(true);
expect(
canProvideStatsForFieldTextBased({
name: 'message',
type: 'string',
esTypes: ['keyword'],
} as DataViewField)
).toBe(true);
expect(
canProvideStatsForFieldTextBased({ name: 'message', type: 'number' } as DataViewField)
).toBe(true);
expect(
canProvideStatsForFieldTextBased({ name: 'message', type: 'boolean' } as DataViewField)
).toBe(true);
expect(
canProvideStatsForFieldTextBased({ name: 'message', type: 'ip' } as DataViewField)
).toBe(true);
expect(
canProvideStatsForFieldTextBased({ name: 'message', type: 'ip_range' } as DataViewField)
).toBe(false);
expect(
canProvideStatsForFieldTextBased({ name: 'message', type: 'geo_point' } as DataViewField)
).toBe(true);
expect(
canProvideStatsForFieldTextBased({ name: 'message', type: 'date' } as DataViewField)
).toBe(false);
expect(
canProvideStatsForFieldTextBased({
name: '_id',
type: 'string',
esTypes: ['keyword'],
} as DataViewField)
).toBe(true);
});
});
});

View file

@ -9,22 +9,22 @@
import type { DataViewField } from '@kbn/data-views-plugin/common';
export function canProvideStatsForField(field: DataViewField, isTextBased: boolean): boolean {
if (isTextBased) {
return canProvideStatsForFieldTextBased(field);
export function canProvideStatsForField(field: DataViewField, isEsqlQuery: boolean): boolean {
if (isEsqlQuery) {
return false;
}
return (
(field.aggregatable && canProvideAggregatedStatsForField(field, isTextBased)) ||
(field.aggregatable && canProvideAggregatedStatsForField(field, isEsqlQuery)) ||
((!field.aggregatable || field.type === 'geo_point' || field.type === 'geo_shape') &&
canProvideExamplesForField(field, isTextBased))
canProvideExamplesForField(field, isEsqlQuery))
);
}
export function canProvideAggregatedStatsForField(
field: DataViewField,
isTextBased: boolean
isEsqlQuery: boolean
): boolean {
if (isTextBased) {
if (isEsqlQuery) {
return false;
}
return !(
@ -39,20 +39,17 @@ export function canProvideAggregatedStatsForField(
export function canProvideNumberSummaryForField(
field: DataViewField,
isTextBased: boolean
isEsqlQuery: boolean
): boolean {
if (isTextBased) {
if (isEsqlQuery) {
return false;
}
return field.timeSeriesMetric === 'counter';
}
export function canProvideExamplesForField(field: DataViewField, isTextBased: boolean): boolean {
if (isTextBased) {
return (
(field.type === 'string' && !canProvideTopValuesForFieldTextBased(field)) ||
['geo_point', 'geo_shape'].includes(field.type)
);
export function canProvideExamplesForField(field: DataViewField, isEsqlQuery: boolean): boolean {
if (isEsqlQuery) {
return false;
}
if (field.name === '_score') {
return false;
@ -69,17 +66,6 @@ export function canProvideExamplesForField(field: DataViewField, isTextBased: bo
].includes(field.type);
}
export function canProvideTopValuesForFieldTextBased(field: DataViewField): boolean {
if (field.name === '_id') {
return false;
}
const esTypes = field.esTypes?.[0];
return (
Boolean(field.type === 'string' && esTypes && ['keyword', 'version'].includes(esTypes)) ||
['keyword', 'version', 'ip', 'number', 'boolean'].includes(field.type)
);
}
export function canProvideStatsForFieldTextBased(field: DataViewField): boolean {
return canProvideTopValuesForFieldTextBased(field) || canProvideExamplesForField(field, true);
export function canProvideStatsForEsqlField(field: DataViewField): boolean {
return false;
}

View file

@ -32,7 +32,6 @@
"@kbn/shared-ux-button-toolbar",
"@kbn/field-utils",
"@kbn/visualization-utils",
"@kbn/esql-utils",
"@kbn/search-types",
"@kbn/fields-metadata-plugin",
"@kbn/ui-theme"

View file

@ -155,77 +155,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await unifiedFieldList.waitUntilSidebarHasLoaded();
});
it('should show top values popover for numeric field', async () => {
it('should not show top values popover for numeric field', async () => {
await unifiedFieldList.clickFieldListItem('bytes');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Top values');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(10);
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'42 sample values'
);
await unifiedFieldList.clickFieldListPlusFilter('bytes', '0');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`bytes\`==0`
);
await testSubjects.missingOrFail('dscFieldStats-statsFooter');
await unifiedFieldList.closeFieldPopover();
});
it('should show a top values popover for a keyword field', async () => {
it('should not show a top values popover for a keyword field', async () => {
await unifiedFieldList.clickFieldListItem('extension.raw');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Top values');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(5);
await testSubjects.missingOrFail('unifiedFieldStats-buttonGroup');
await testSubjects.missingOrFail('unifiedFieldStats-histogram');
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'500 sample values'
);
await unifiedFieldList.clickFieldListPlusFilter('extension.raw', 'css');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`extension.raw\`=="css"`
);
await unifiedFieldList.closeFieldPopover();
});
it('should show a top values popover for an ip field', async () => {
await unifiedFieldList.clickFieldListItem('clientip');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Top values');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(10);
await testSubjects.missingOrFail('unifiedFieldStats-buttonGroup');
await testSubjects.missingOrFail('unifiedFieldStats-histogram');
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'32 sample values'
);
await unifiedFieldList.clickFieldListPlusFilter('clientip', '216.126.255.31');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`clientip\`::string=="216.126.255.31"`
);
await unifiedFieldList.closeFieldPopover();
});
it('should show a top values popover for _index field', async () => {
await unifiedFieldList.clickFieldListItem('_index');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Top values');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(1);
await testSubjects.missingOrFail('unifiedFieldStats-buttonGroup');
await testSubjects.missingOrFail('unifiedFieldStats-histogram');
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'500 sample values'
);
await testSubjects.missingOrFail('dscFieldStats-statsFooter');
await unifiedFieldList.closeFieldPopover();
});
@ -240,102 +178,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await unifiedFieldList.closeFieldPopover();
});
it('should show examples for geo points field', async () => {
await unifiedFieldList.clickFieldListItem('geo.coordinates');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Examples');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(11);
await testSubjects.missingOrFail('unifiedFieldStats-buttonGroup');
await testSubjects.missingOrFail('unifiedFieldStats-histogram');
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'100 sample records'
);
await unifiedFieldList.closeFieldPopover();
});
it('should show examples for text field', async () => {
it('should not show examples for text field', async () => {
await unifiedFieldList.clickFieldListItem('extension');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Examples');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(5);
await testSubjects.missingOrFail('unifiedFieldStats-buttonGroup');
await testSubjects.missingOrFail('unifiedFieldStats-histogram');
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'100 sample records'
);
await unifiedFieldList.clickFieldListPlusFilter('extension', 'css');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* [METADATA _index, _id] | sort @timestamp desc | limit 500\n| WHERE \`extension\`=="css"`
);
await unifiedFieldList.closeFieldPopover();
});
it('should show examples for _id field', async () => {
await unifiedFieldList.clickFieldListItem('_id');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Examples');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(11);
await testSubjects.missingOrFail('unifiedFieldStats-buttonGroup');
await testSubjects.missingOrFail('unifiedFieldStats-histogram');
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'100 sample records'
);
await unifiedFieldList.closeFieldPopover();
});
it('should show a top values popover for a more complex query', async () => {
const testQuery = `from logstash-* | sort @timestamp desc | limit 50 | stats avg(bytes) by geo.dest | limit 3`;
await monacoEditor.setCodeEditorValue(testQuery);
await testSubjects.click('querySubmitButton');
await header.waitUntilLoadingHasFinished();
await unifiedFieldList.waitUntilSidebarHasLoaded();
await unifiedFieldList.clickFieldListItem('avg(bytes)');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Top values');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(3);
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'3 sample values'
);
await unifiedFieldList.clickFieldListPlusFilter('avg(bytes)', '5453');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(
`from logstash-* | sort @timestamp desc | limit 50 | stats avg(bytes) by geo.dest | limit 3\n| WHERE \`avg(bytes)\`==5453`
);
await unifiedFieldList.closeFieldPopover();
});
it('should show a top values popover for a boolean field', async () => {
const testQuery = `row enabled = true`;
await monacoEditor.setCodeEditorValue(testQuery);
await testSubjects.click('querySubmitButton');
await header.waitUntilLoadingHasFinished();
await unifiedFieldList.waitUntilSidebarHasLoaded();
await unifiedFieldList.clickFieldListItem('enabled');
await testSubjects.existOrFail('dscFieldStats-topValues');
expect(await testSubjects.getVisibleText('dscFieldStats-title')).to.be('Top values');
const topValuesRows = await testSubjects.findAll('dscFieldStats-topValues-bucket');
expect(topValuesRows.length).to.eql(1);
expect(await unifiedFieldList.getFieldStatsTopValueBucketsVisibleText()).to.be(
'true\n100%'
);
expect(await testSubjects.getVisibleText('dscFieldStats-statsFooter')).to.contain(
'1 sample value'
);
await unifiedFieldList.clickFieldListMinusFilter('enabled', 'true');
const editorValue = await monacoEditor.getCodeEditorValue();
expect(editorValue).to.eql(`row enabled = true\n| WHERE \`enabled\`!=true`);
await testSubjects.missingOrFail('dscFieldStats-statsFooter');
await unifiedFieldList.closeFieldPopover();
});
});

View file

@ -8463,7 +8463,6 @@
"unifiedFieldList.fieldsAccordion.existenceTimeoutLabel": "Les informations de champ ont pris trop de temps",
"unifiedFieldList.fieldStats.bucketPercentageTooltip": "{formattedPercentage} ({count, plural, one {# enregistrement} other {# enregistrements}})",
"unifiedFieldList.fieldStats.calculatedFromSampleRecordsLabel": "Calculé à partir de {sampledDocumentsFormatted} {sampledDocuments, plural, one {exemple d'enregistrement} other {exemples d'enregistrement}}.",
"unifiedFieldList.fieldStats.calculatedFromSampleValuesLabel": "Calculé à partir {sampledValuesFormatted} {sampledValues, plural, one {d'un exemple de valeur} other {dexemples de valeur}}.",
"unifiedFieldList.fieldStats.calculatedFromTotalRecordsLabel": "Calculé à partir de {totalDocumentsFormatted} {totalDocuments, plural, one {enregistrement} other {enregistrements}}.",
"unifiedFieldList.fieldStats.countLabel": "Décompte",
"unifiedFieldList.fieldStats.displayToggleLegend": "Basculer soit",

View file

@ -8452,7 +8452,6 @@
"unifiedFieldList.fieldsAccordion.existenceTimeoutLabel": "フィールド情報に時間がかかりすぎました",
"unifiedFieldList.fieldStats.bucketPercentageTooltip": "{formattedPercentage} ({count, plural, other {# レコード}})",
"unifiedFieldList.fieldStats.calculatedFromSampleRecordsLabel": "{sampledDocumentsFormatted}サンプル{sampledDocuments, plural, other {レコード}}から計算されました。",
"unifiedFieldList.fieldStats.calculatedFromSampleValuesLabel": "{sampledValuesFormatted}サンプル{sampledValues, plural, other {値}}から計算されました。",
"unifiedFieldList.fieldStats.calculatedFromTotalRecordsLabel": "{totalDocumentsFormatted} {totalDocuments, plural, other {レコード}}から計算されました。",
"unifiedFieldList.fieldStats.countLabel": "カウント",
"unifiedFieldList.fieldStats.displayToggleLegend": "次のどちらかを切り替えます:",

View file

@ -8470,7 +8470,6 @@
"unifiedFieldList.fieldsAccordion.existenceTimeoutLabel": "字段信息花费时间过久",
"unifiedFieldList.fieldStats.bucketPercentageTooltip": "{formattedPercentage}{count, plural, other {# 条记录}}",
"unifiedFieldList.fieldStats.calculatedFromSampleRecordsLabel": "基于 {sampledDocumentsFormatted} 个样例{sampledDocuments, plural, other {记录}}计算。",
"unifiedFieldList.fieldStats.calculatedFromSampleValuesLabel": "基于 {sampledValuesFormatted} 个样例{sampledValues, plural, other {值}}计算。",
"unifiedFieldList.fieldStats.calculatedFromTotalRecordsLabel": "基于 {totalDocumentsFormatted} 个样例{totalDocuments, plural, other {记录}}计算。",
"unifiedFieldList.fieldStats.countLabel": "计数",
"unifiedFieldList.fieldStats.displayToggleLegend": "切换",