Skip to content

Commit

Permalink
[Discover] Breakdown support for fieldstats (elastic#199028)
Browse files Browse the repository at this point in the history
closes elastic#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 <[email protected]>
  • Loading branch information
2 people authored and tkajtoch committed Nov 12, 2024
1 parent e6215fd commit c1bdaab
Show file tree
Hide file tree
Showing 24 changed files with 307 additions and 62 deletions.
1 change: 1 addition & 0 deletions packages/kbn-esql-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export {
getQueryColumnsFromESQLQuery,
isESQLColumnSortable,
isESQLColumnGroupable,
isESQLFieldGroupable,
TextBasedLanguages,
} from './src';

Expand Down
6 changes: 5 additions & 1 deletion packages/kbn-esql-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
49 changes: 48 additions & 1 deletion packages/kbn-esql-utils/src/utils/esql_fields_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
29 changes: 19 additions & 10 deletions packages/kbn-esql-utils/src/utils/esql_fields_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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 isGroupable(column.meta?.type, column.meta?.esType);
};

return true;
export const isESQLFieldGroupable = (field: FieldSpec): boolean => {
if (field.timeSeriesMetric === 'counter') return false;
return isGroupable(field.type, field.esTypes?.[0]);
};
1 change: 1 addition & 0 deletions packages/kbn-field-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,17 @@ describe('UnifiedFieldList <FieldPopoverHeader />', () => {
field={field}
closePopover={mockClose}
onAddFieldToWorkspace={jest.fn()}
onAddBreakdownField={jest.fn()}
onAddFilter={jest.fn()}
onEditField={jest.fn()}
onDeleteField={jest.fn()}
/>
);

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();
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,6 +48,7 @@ export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
buttonAddFilterProps,
buttonEditFieldProps,
buttonDeleteFieldProps,
onAddBreakdownField,
onAddFieldToWorkspace,
onAddFilter,
onEditField,
Expand Down Expand Up @@ -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}>
Expand All @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ??
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -215,6 +221,7 @@ function UnifiedFieldListItemComponent({
field,
highlight,
dataView,
onAddBreakdownField,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
onAddFilter,
Expand All @@ -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
Expand Down Expand Up @@ -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}
/>
)}
Expand Down
Loading

0 comments on commit c1bdaab

Please sign in to comment.