[Discover] Breakdown support for fieldstats (#199028)

closes https://github.com/elastic/kibana/issues/192700

## 📝  Summary

This PR add a new `Add breakdown` button to the field stats popover for
all applicable fields.

## 🎥 Demo


https://github.com/user-attachments/assets/d647189c-9b04-4127-a4fd-f9764babe46e

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
mohamedhamed-ahmed 2024-11-12 12:26:36 +00:00 committed by GitHub
parent 763b5deafd
commit 73694426f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 313 additions and 68 deletions

View file

@ -31,6 +31,7 @@ export {
getQueryColumnsFromESQLQuery,
isESQLColumnSortable,
isESQLColumnGroupable,
isESQLFieldGroupable,
TextBasedLanguages,
} from './src';

View file

@ -31,4 +31,8 @@ export {
getStartEndParams,
hasStartEndParams,
} from './utils/run_query';
export { isESQLColumnSortable, isESQLColumnGroupable } from './utils/esql_fields_utils';
export {
isESQLColumnSortable,
isESQLColumnGroupable,
isESQLFieldGroupable,
} from './utils/esql_fields_utils';

View file

@ -7,7 +7,12 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { isESQLColumnSortable, isESQLColumnGroupable } from './esql_fields_utils';
import {
isESQLColumnSortable,
isESQLColumnGroupable,
isESQLFieldGroupable,
} from './esql_fields_utils';
import type { FieldSpec } from '@kbn/data-views-plugin/common';
describe('esql fields helpers', () => {
describe('isESQLColumnSortable', () => {
@ -104,4 +109,46 @@ describe('esql fields helpers', () => {
expect(isESQLColumnGroupable(keywordField)).toBeTruthy();
});
});
describe('isESQLFieldGroupable', () => {
it('returns false for unsupported fields', () => {
const fieldSpec: FieldSpec = {
name: 'unsupported',
type: 'unknown',
esTypes: ['unknown'],
searchable: true,
aggregatable: false,
isNull: false,
};
expect(isESQLFieldGroupable(fieldSpec)).toBeFalsy();
});
it('returns false for counter fields', () => {
const fieldSpec: FieldSpec = {
name: 'tsbd_counter',
type: 'number',
esTypes: ['long'],
timeSeriesMetric: 'counter',
searchable: true,
aggregatable: false,
isNull: false,
};
expect(isESQLFieldGroupable(fieldSpec)).toBeFalsy();
});
it('returns true for everything else', () => {
const fieldSpec: FieldSpec = {
name: 'sortable',
type: 'string',
esTypes: ['keyword'],
searchable: true,
aggregatable: false,
isNull: false,
};
expect(isESQLFieldGroupable(fieldSpec)).toBeTruthy();
});
});
});

View file

@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import type { FieldSpec } from '@kbn/data-views-plugin/common';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
const SPATIAL_FIELDS = ['geo_point', 'geo_shape', 'point', 'shape'];
@ -40,22 +41,30 @@ export const isESQLColumnSortable = (column: DatatableColumn): boolean => {
return true;
};
// Helper function to check if a field is groupable based on its type and esType
const isGroupable = (type: string | undefined, esType: string | undefined): boolean => {
// we don't allow grouping on the unknown field types
if (type === UNKNOWN_FIELD) {
return false;
}
// we don't allow grouping on tsdb counter fields
if (esType && esType.indexOf(TSDB_COUNTER_FIELDS_PREFIX) !== -1) {
return false;
}
return true;
};
/**
* Check if a column is groupable (| STATS ... BY <column>).
*
* @param column The DatatableColumn of the field.
* @returns True if the column is groupable, false otherwise.
*/
export const isESQLColumnGroupable = (column: DatatableColumn): boolean => {
// we don't allow grouping on the unknown field types
if (column.meta?.type === UNKNOWN_FIELD) {
return false;
}
// we don't allow grouping on tsdb counter fields
if (column.meta?.esType && column.meta?.esType?.indexOf(TSDB_COUNTER_FIELDS_PREFIX) !== -1) {
return false;
}
return true;
return isGroupable(column.meta?.type, column.meta?.esType);
};
export const isESQLFieldGroupable = (field: FieldSpec): boolean => {
if (field.timeSeriesMetric === 'counter') return false;
return isGroupable(field.type, field.esTypes?.[0]);
};

View file

@ -25,6 +25,7 @@ export {
comboBoxFieldOptionMatcher,
getFieldSearchMatchingHighlight,
} from './src/utils/field_name_wildcard_matcher';
export { fieldSupportsBreakdown } from './src/utils/field_supports_breakdown';
export { FieldIcon, type FieldIconProps, getFieldIconProps } from './src/components/field_icon';
export { FieldDescription, type FieldDescriptionProps } from './src/components/field_description';

View file

@ -7,12 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { DataViewField } from '@kbn/data-views-plugin/public';
import { type DataViewField } from '@kbn/data-views-plugin/common';
import { KNOWN_FIELD_TYPES } from './field_types';
const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']);
const supportedTypes = new Set([
KNOWN_FIELD_TYPES.STRING,
KNOWN_FIELD_TYPES.BOOLEAN,
KNOWN_FIELD_TYPES.NUMBER,
KNOWN_FIELD_TYPES.IP,
]);
export const fieldSupportsBreakdown = (field: DataViewField) =>
supportedTypes.has(field.type) &&
supportedTypes.has(field.type as KNOWN_FIELD_TYPES) &&
field.aggregatable &&
!field.scripted &&
field.timeSeriesMetric !== 'counter';

View file

@ -38,6 +38,7 @@ describe('UnifiedFieldList <FieldPopoverHeader />', () => {
field={field}
closePopover={mockClose}
onAddFieldToWorkspace={jest.fn()}
onAddBreakdownField={jest.fn()}
onAddFilter={jest.fn()}
onEditField={jest.fn()}
onDeleteField={jest.fn()}
@ -45,6 +46,9 @@ describe('UnifiedFieldList <FieldPopoverHeader />', () => {
);
expect(wrapper.text()).toBe(fieldName);
expect(
wrapper.find(`[data-test-subj="fieldPopoverHeader_addBreakdownField-${fieldName}"]`).exists()
).toBeTruthy();
expect(
wrapper.find(`[data-test-subj="fieldPopoverHeader_addField-${fieldName}"]`).exists()
).toBeTruthy();
@ -57,7 +61,29 @@ describe('UnifiedFieldList <FieldPopoverHeader />', () => {
expect(
wrapper.find(`[data-test-subj="fieldPopoverHeader_deleteField-${fieldName}"]`).exists()
).toBeTruthy();
expect(wrapper.find(EuiButtonIcon)).toHaveLength(4);
expect(wrapper.find(EuiButtonIcon)).toHaveLength(5);
});
it('should correctly handle add-breakdown-field action', async () => {
const mockClose = jest.fn();
const mockAddBreakdownField = jest.fn();
const fieldName = 'extension';
const field = dataView.fields.find((f) => f.name === fieldName)!;
const wrapper = mountWithIntl(
<FieldPopoverHeader
field={field}
closePopover={mockClose}
onAddBreakdownField={mockAddBreakdownField}
/>
);
wrapper
.find(`[data-test-subj="fieldPopoverHeader_addBreakdownField-${fieldName}"]`)
.first()
.simulate('click');
expect(mockClose).toHaveBeenCalled();
expect(mockAddBreakdownField).toHaveBeenCalledWith(field);
});
it('should correctly handle add-field action', async () => {

View file

@ -31,6 +31,7 @@ export interface FieldPopoverHeaderProps {
buttonAddFilterProps?: Partial<EuiButtonIconProps>;
buttonEditFieldProps?: Partial<EuiButtonIconProps>;
buttonDeleteFieldProps?: Partial<EuiButtonIconProps>;
onAddBreakdownField?: (field: DataViewField | undefined) => void;
onAddFieldToWorkspace?: (field: DataViewField) => unknown;
onAddFilter?: AddFieldFilterHandler;
onEditField?: (fieldName: string) => unknown;
@ -47,6 +48,7 @@ export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
buttonAddFilterProps,
buttonEditFieldProps,
buttonDeleteFieldProps,
onAddBreakdownField,
onAddFieldToWorkspace,
onAddFilter,
onEditField,
@ -82,6 +84,13 @@ export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
defaultMessage: 'Delete data view field',
});
const addBreakdownFieldTooltip = i18n.translate(
'unifiedFieldList.fieldPopover.addBreakdownFieldLabel',
{
defaultMessage: 'Add breakdown',
}
);
return (
<>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
@ -108,6 +117,21 @@ export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
</EuiToolTip>
</EuiFlexItem>
)}
{onAddBreakdownField && (
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addBreakdownField">
<EuiToolTip content={addBreakdownFieldTooltip}>
<EuiButtonIcon
data-test-subj={`fieldPopoverHeader_addBreakdownField-${field.name}`}
aria-label={addBreakdownFieldTooltip}
iconType="visBarVerticalStacked"
onClick={() => {
closePopover();
onAddBreakdownField(field);
}}
/>
</EuiToolTip>
</EuiFlexItem>
)}
{onAddFilter && field.filterable && !field.scripted && (
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addExistsFilter">
<EuiToolTip content={buttonAddFilterProps?.['aria-label'] ?? addExistsFilterTooltip}>

View file

@ -43,10 +43,12 @@ async function getComponent({
selected = false,
field,
canFilter = true,
isBreakdownSupported = true,
}: {
selected?: boolean;
field?: DataViewField;
canFilter?: boolean;
isBreakdownSupported?: boolean;
}) {
const finalField =
field ??
@ -76,6 +78,7 @@ async function getComponent({
dataView: stubDataView,
field: finalField,
...(canFilter && { onAddFilter: jest.fn() }),
...(isBreakdownSupported && { onAddBreakdownField: jest.fn() }),
onAddFieldToWorkspace: jest.fn(),
onRemoveFieldFromWorkspace: jest.fn(),
onEditField: jest.fn(),
@ -137,6 +140,34 @@ describe('UnifiedFieldListItem', function () {
expect(comp.find(FieldItemButton).prop('onClick')).toBeUndefined();
});
it('should not show addBreakdownField action button if not supported', async function () {
const field = new DataViewField({
name: 'extension.keyword',
type: 'string',
esTypes: ['keyword'],
aggregatable: true,
searchable: true,
});
const { comp } = await getComponent({
field,
isBreakdownSupported: false,
});
await act(async () => {
const fieldItem = findTestSubject(comp, 'field-extension.keyword-showDetails');
await fieldItem.simulate('click');
await comp.update();
});
await comp.update();
expect(
comp
.find('[data-test-subj="fieldPopoverHeader_addBreakdownField-extension.keyword"]')
.exists()
).toBeFalsy();
});
it('should request field stats', async function () {
const field = new DataViewField({
name: 'machine.os.raw',
@ -189,6 +220,11 @@ describe('UnifiedFieldListItem', function () {
await comp.update();
expect(comp.find(EuiPopover).prop('isOpen')).toBe(true);
expect(
comp
.find('[data-test-subj="fieldPopoverHeader_addBreakdownField-extension.keyword"]')
.exists()
).toBeTruthy();
expect(
comp.find('[data-test-subj="fieldPopoverHeader_addField-extension.keyword"]').exists()
).toBeTruthy();

View file

@ -15,6 +15,8 @@ import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/publ
import { Draggable } from '@kbn/dom-drag-drop';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { Filter } from '@kbn/es-query';
import { fieldSupportsBreakdown } from '@kbn/field-utils';
import { isESQLFieldGroupable } from '@kbn/esql-utils';
import type { SearchMode } from '../../types';
import { FieldItemButton, type FieldItemButtonProps } from '../../components/field_item_button';
import {
@ -140,6 +142,10 @@ export interface UnifiedFieldListItemProps {
* The currently selected data view
*/
dataView: DataView;
/**
* Callback to update breakdown field
*/
onAddBreakdownField?: (breakdownField: DataViewField | undefined) => void;
/**
* Callback to add/select the field
*/
@ -215,6 +221,7 @@ function UnifiedFieldListItemComponent({
field,
highlight,
dataView,
onAddBreakdownField,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
onAddFilter,
@ -232,6 +239,9 @@ function UnifiedFieldListItemComponent({
}: UnifiedFieldListItemProps) {
const [infoIsOpen, setOpen] = useState(false);
const isBreakdownSupported =
searchMode === 'documents' ? fieldSupportsBreakdown(field) : isESQLFieldGroupable(field);
const addFilterAndClosePopover: typeof onAddFilter | undefined = useMemo(
() =>
onAddFilter
@ -394,13 +404,14 @@ function UnifiedFieldListItemComponent({
data-test-subj={stateService.creationOptions.dataTestSubj?.fieldListItemPopoverDataTestSubj}
renderHeader={() => (
<FieldPopoverHeader
services={services}
field={field}
closePopover={closePopover}
field={field}
onAddBreakdownField={isBreakdownSupported ? onAddBreakdownField : undefined}
onAddFieldToWorkspace={!isSelected ? toggleDisplay : undefined}
onAddFilter={onAddFilter}
onEditField={onEditField}
onDeleteField={onDeleteField}
onEditField={onEditField}
services={services}
{...customPopoverHeaderProps}
/>
)}

View file

@ -48,6 +48,7 @@ export type UnifiedFieldListSidebarCustomizableProps = Pick<
| 'dataView'
| 'trackUiMetric'
| 'onAddFilter'
| 'onAddBreakdownField'
| 'onAddFieldToWorkspace'
| 'onRemoveFieldFromWorkspace'
| 'additionalFilters'
@ -161,6 +162,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
fullWidth,
isAffectedByGlobalFilter,
prepend,
onAddBreakdownField,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
onAddFilter,
@ -264,30 +266,31 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
({ field, groupName, groupIndex, itemIndex, fieldSearchHighlight }) => (
<li key={`field${field.name}`} data-attr-field={field.name}>
<UnifiedFieldListItem
stateService={stateService}
searchMode={searchMode}
services={services}
additionalFilters={additionalFilters}
alwaysShowActionButton={alwaysShowActionButton}
field={field}
size={compressed ? 'xs' : 's'}
highlight={fieldSearchHighlight}
dataView={dataView!}
onAddFieldToWorkspace={onAddFieldToWorkspace}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
onAddFilter={onAddFilter}
trackUiMetric={trackUiMetric}
multiFields={multiFieldsMap?.get(field.name)} // ideally we better calculate multifields when they are requested first from the popover
onEditField={onEditField}
onDeleteField={onDeleteField}
workspaceSelectedFieldNames={workspaceSelectedFieldNames}
field={field}
groupIndex={groupIndex}
itemIndex={itemIndex}
highlight={fieldSearchHighlight}
isEmpty={groupName === FieldsGroupNames.EmptyFields}
isSelected={
groupName === FieldsGroupNames.SelectedFields ||
Boolean(selectedFieldsState.selectedFieldsMap[field.name])
}
additionalFilters={additionalFilters}
itemIndex={itemIndex}
multiFields={multiFieldsMap?.get(field.name)} // ideally we better calculate multifields when they are requested first from the popover
onAddBreakdownField={onAddBreakdownField}
onAddFieldToWorkspace={onAddFieldToWorkspace}
onAddFilter={onAddFilter}
onDeleteField={onDeleteField}
onEditField={onEditField}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
searchMode={searchMode}
services={services}
size={compressed ? 'xs' : 's'}
stateService={stateService}
trackUiMetric={trackUiMetric}
workspaceSelectedFieldNames={workspaceSelectedFieldNames}
/>
</li>
),
@ -298,6 +301,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
alwaysShowActionButton,
compressed,
dataView,
onAddBreakdownField,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
onAddFilter,

View file

@ -34,7 +34,8 @@
"@kbn/visualization-utils",
"@kbn/search-types",
"@kbn/fields-metadata-plugin",
"@kbn/ui-theme"
"@kbn/ui-theme",
"@kbn/esql-utils",
],
"exclude": ["target/**/*"]
}

View file

@ -20,12 +20,12 @@ import {
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { isOfAggregateQueryType } from '@kbn/es-query';
import { appendWhereClauseToESQLQuery } from '@kbn/esql-utils';
import { appendWhereClauseToESQLQuery, hasTransformationalCommand } from '@kbn/esql-utils';
import { METRIC_TYPE } from '@kbn/analytics';
import classNames from 'classnames';
import { generateFilters } from '@kbn/data-plugin/public';
import { useDragDropContext } from '@kbn/dom-drag-drop';
import { DataViewType } from '@kbn/data-views-plugin/public';
import { type DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
import {
SEARCH_FIELDS_FROM_SOURCE,
SHOW_FIELD_STATISTICS,
@ -256,6 +256,21 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
const onFilter = isEsqlMode ? onPopulateWhereClause : onAddFilter;
const canSetBreakdownField = useMemo(
() =>
isOfAggregateQueryType(query)
? dataView?.isTimeBased() && !hasTransformationalCommand(query.esql)
: true,
[dataView, query]
);
const onAddBreakdownField = useCallback(
(field: DataViewField | undefined) => {
stateContainer.appState.update({ breakdownField: field?.name });
},
[stateContainer]
);
const onFieldEdited = useCallback(
async ({ removedFieldName }: { removedFieldName?: string } = {}) => {
if (removedFieldName && currentColumns.includes(removedFieldName)) {
@ -423,18 +438,19 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
sidebarToggleState$={sidebarToggleState$}
sidebarPanel={
<SidebarMemoized
documents$={stateContainer.dataState.data$.documents$}
onAddField={onAddColumnWithTracking}
onRemoveField={onRemoveColumnWithTracking}
additionalFilters={customFilters}
columns={currentColumns}
documents$={stateContainer.dataState.data$.documents$}
onAddBreakdownField={canSetBreakdownField ? onAddBreakdownField : undefined}
onAddField={onAddColumnWithTracking}
onAddFilter={onFilter}
onChangeDataView={stateContainer.actions.onChangeDataView}
selectedDataView={dataView}
trackUiMetric={trackUiMetric}
onFieldEdited={onFieldEdited}
onDataViewCreated={stateContainer.actions.onDataViewCreated}
onFieldEdited={onFieldEdited}
onRemoveField={onRemoveColumnWithTracking}
selectedDataView={dataView}
sidebarToggleState$={sidebarToggleState$}
additionalFilters={customFilters}
trackUiMetric={trackUiMetric}
/>
}
mainPanel={

View file

@ -162,6 +162,7 @@ function getCompProps(options?: { hits?: DataTableRecord[] }): DiscoverSidebarRe
result: hits,
}) as DataDocuments$,
onChangeDataView: jest.fn(),
onAddBreakdownField: jest.fn(),
onAddFilter: jest.fn(),
onAddField: jest.fn(),
onRemoveField: jest.fn(),
@ -397,6 +398,19 @@ describe('discover responsive sidebar', function () {
expect(ExistingFieldsServiceApi.loadFieldExisting).not.toHaveBeenCalled();
});
it('should allow adding breakdown field', async function () {
const comp = await mountComponent(props);
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');
await act(async () => {
const button = findTestSubject(availableFields, 'field-extension-showDetails');
button.simulate('click');
comp.update();
});
comp.update();
findTestSubject(comp, 'fieldPopoverHeader_addBreakdownField-extension').simulate('click');
expect(props.onAddBreakdownField).toHaveBeenCalled();
});
it('should allow selecting fields', async function () {
const comp = await mountComponent(props);
const availableFields = findTestSubject(comp, 'fieldListGroupedAvailableFields');

View file

@ -87,6 +87,10 @@ export interface DiscoverSidebarResponsiveProps {
* hits fetched from ES, displayed in the doc table
*/
documents$: DataDocuments$;
/**
* Callback to update breakdown field
*/
onAddBreakdownField?: (breakdownField: DataViewField | undefined) => void;
/**
* Callback function when selecting a field
*/
@ -151,6 +155,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
selectedDataView,
columns,
trackUiMetric,
onAddBreakdownField,
onAddFilter,
onFieldEdited,
onDataViewCreated,
@ -373,23 +378,24 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
<EuiFlexItem>
{selectedDataView ? (
<UnifiedFieldListSidebarContainer
ref={initializeUnifiedFieldListSidebarContainerApi}
variant={fieldListVariant}
getCreationOptions={getCreationOptions}
services={services}
dataView={selectedDataView}
trackUiMetric={trackUiMetric}
allFields={sidebarState.allFields}
showFieldList={showFieldList}
workspaceSelectedFieldNames={columns}
fullWidth
onAddFieldToWorkspace={onAddFieldToWorkspace}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
onAddFilter={onAddFilter}
onFieldEdited={onFieldEdited}
prependInFlyout={prependDataViewPickerForMobile}
additionalFieldGroups={additionalFieldGroups}
additionalFilters={additionalFilters}
allFields={sidebarState.allFields}
dataView={selectedDataView}
fullWidth
getCreationOptions={getCreationOptions}
onAddBreakdownField={onAddBreakdownField}
onAddFieldToWorkspace={onAddFieldToWorkspace}
onAddFilter={onAddFilter}
onFieldEdited={onFieldEdited}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
prependInFlyout={prependDataViewPickerForMobile}
ref={initializeUnifiedFieldListSidebarContainerApi}
services={services}
showFieldList={showFieldList}
trackUiMetric={trackUiMetric}
variant={fieldListVariant}
workspaceSelectedFieldNames={columns}
/>
) : null}
</EuiFlexItem>

View file

@ -9,7 +9,12 @@
import React, { useCallback, useMemo } from 'react';
import { EuiSelectableOption } from '@elastic/eui';
import { FieldIcon, getFieldIconProps, comboBoxFieldOptionMatcher } from '@kbn/field-utils';
import {
FieldIcon,
getFieldIconProps,
comboBoxFieldOptionMatcher,
fieldSupportsBreakdown,
} from '@kbn/field-utils';
import { css } from '@emotion/react';
import { isESQLColumnGroupable } from '@kbn/esql-utils';
import { type DataView, DataViewField } from '@kbn/data-views-plugin/common';
@ -17,7 +22,6 @@ import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { i18n } from '@kbn/i18n';
import { UnifiedHistogramBreakdownContext } from '../types';
import { fieldSupportsBreakdown } from '../utils/field_supports_breakdown';
import {
ToolbarSelector,
ToolbarSelectorProps,

View file

@ -36,6 +36,5 @@ export type {
} from './types';
export { UnifiedHistogramFetchStatus, UnifiedHistogramExternalVisContextStatus } from './types';
export { canImportVisContext } from './utils/external_vis_context';
export { fieldSupportsBreakdown } from './utils/field_supports_breakdown';
export const plugin = () => new UnifiedHistogramPublicPlugin();

View file

@ -36,6 +36,7 @@ import { LegendSize } from '@kbn/visualizations-plugin/public';
import { XYConfiguration } from '@kbn/visualizations-plugin/common';
import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { fieldSupportsBreakdown } from '@kbn/field-utils';
import {
UnifiedHistogramExternalVisContextStatus,
UnifiedHistogramSuggestionContext,
@ -49,7 +50,6 @@ import {
injectESQLQueryIntoLensLayers,
} from '../utils/external_vis_context';
import { computeInterval } from '../utils/compute_interval';
import { fieldSupportsBreakdown } from '../utils/field_supports_breakdown';
import { shouldDisplayHistogram } from '../layout/helpers';
import { enrichLensAttributesWithTablesData } from '../utils/lens_vis_from_table';

View file

@ -843,6 +843,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const list = await discover.getHistogramLegendList();
expect(list).to.eql(['css', 'gif', 'jpg', 'php', 'png']);
});
it('should choose breakdown field when selected from field stats', async () => {
await discover.selectTextBaseLang();
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
const testQuery = 'from logstash-*';
await monacoEditor.setCodeEditorValue(testQuery);
await testSubjects.click('querySubmitButton');
await header.waitUntilLoadingHasFinished();
await discover.waitUntilSearchingHasFinished();
await unifiedFieldList.clickFieldListAddBreakdownField('extension');
await header.waitUntilLoadingHasFinished();
const list = await discover.getHistogramLegendList();
expect(list).to.eql(['css', 'gif', 'jpg', 'php', 'png']);
});
});
});
}

View file

@ -14,11 +14,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const filterBar = getService('filterBar');
const { common, discover, header, timePicker } = getPageObjects([
const { common, discover, header, timePicker, unifiedFieldList } = getPageObjects([
'common',
'discover',
'header',
'timePicker',
'unifiedFieldList',
]);
describe('discover unified histogram breakdown', function describeIndexTests() {
@ -36,6 +37,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
});
it('should apply breakdown when selected from field stats', async () => {
await unifiedFieldList.clickFieldListAddBreakdownField('geo.dest');
await header.waitUntilLoadingHasFinished();
const list = await discover.getHistogramLegendList();
expect(list).to.eql(['CN', 'IN', 'US', 'Other']);
});
it('should choose breakdown field', async () => {
await discover.chooseBreakdownField('extension.raw');
await header.waitUntilLoadingHasFinished();

View file

@ -226,6 +226,16 @@ export class UnifiedFieldListPageObject extends FtrService {
await this.testSubjects.missingOrFail(`fieldVisualize-${field}`);
}
public async clickFieldListAddBreakdownField(field: string) {
const addBreakdownFieldTestSubj = `fieldPopoverHeader_addBreakdownField-${field}`;
if (!(await this.testSubjects.exists(addBreakdownFieldTestSubj))) {
// field has to be open
await this.clickFieldListItem(field);
}
await this.testSubjects.click(addBreakdownFieldTestSubj);
await this.header.waitUntilLoadingHasFinished();
}
public async clickFieldListPlusFilter(field: string, value: string) {
const plusFilterTestSubj = `plus-${field}-${value}`;
if (!(await this.testSubjects.exists(plusFilterTestSubj))) {

View file

@ -7,10 +7,10 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Action } from '@kbn/ui-actions-plugin/public';
import { fieldSupportsBreakdown } from '@kbn/unified-histogram-plugin/public';
import { i18n } from '@kbn/i18n';
import { useEuiTheme } from '@elastic/eui';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { fieldSupportsBreakdown } from '@kbn/field-utils';
import { DEFAULT_LOGS_DATA_VIEW } from '../../common/constants';
import { useCreateDataView } from './use_create_dataview';
import { useKibanaContextForPlugin } from '../utils';

View file

@ -60,7 +60,8 @@
"@kbn/usage-collection-plugin",
"@kbn/rison",
"@kbn/task-manager-plugin",
"@kbn/core-application-browser"
"@kbn/core-application-browser",
"@kbn/field-utils"
],
"exclude": [
"target/**/*"