mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
f132d6bb49
commit
782b693956
17 changed files with 82 additions and 776 deletions
|
@ -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.');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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`}>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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}"`
|
||||
);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}\``;
|
||||
}
|
|
@ -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';
|
|
@ -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 });
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {d’exemples 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",
|
||||
|
|
|
@ -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": "次のどちらかを切り替えます:",
|
||||
|
|
|
@ -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": "切换",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue