Skip to content

Commit

Permalink
[Discover] Breakdown support for fieldstats
Browse files Browse the repository at this point in the history
  • Loading branch information
mohamedhamed-ahmed committed Nov 5, 2024
1 parent ac04952 commit 3c51e10
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 56 deletions.
26 changes: 16 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 { DataViewField } 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,27 @@ 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 => {
if (type === UNKNOWN_FIELD) {
return false;
}
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: DataViewField): boolean => {
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 @@ -27,10 +27,12 @@ import type { AddFieldFilterHandler } from '../../types';
export interface FieldPopoverHeaderProps {
field: DataViewField;
closePopover: EuiPopoverProps['closePopover'];
buttonAddBreakdownFieldProps?: Partial<EuiButtonIconProps>;
buttonAddFieldToWorkspaceProps?: Partial<EuiButtonIconProps>;
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 @@ -43,10 +45,12 @@ export interface FieldPopoverHeaderProps {
export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
field,
closePopover,
buttonAddBreakdownFieldProps,
buttonAddFieldToWorkspaceProps,
buttonAddFilterProps,
buttonEditFieldProps,
buttonDeleteFieldProps,
onAddBreakdownField,
onAddFieldToWorkspace,
onAddFilter,
onEditField,
Expand Down Expand Up @@ -82,6 +86,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 @@ -90,6 +101,24 @@ export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
<h5 className="eui-textBreakWord">{field.displayName}</h5>
</EuiTitle>
</EuiFlexItem>
{onAddBreakdownField && (
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addBreakdownField">
<EuiToolTip
content={buttonAddBreakdownFieldProps?.['aria-label'] ?? addBreakdownFieldTooltip}
>
<EuiButtonIcon
data-test-subj={`fieldPopoverHeader_addBreakdownField-${field.name}`}
aria-label={addBreakdownFieldTooltip}
{...(buttonAddBreakdownFieldProps || {})}
iconType="visPie"
onClick={() => {
closePopover();
onAddBreakdownField(field);
}}
/>
</EuiToolTip>
</EuiFlexItem>
)}
{onAddFieldToWorkspace && (
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addField">
<EuiToolTip
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/src/utils/esql_fields_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
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type UnifiedFieldListSidebarCustomizableProps = Pick<
| 'dataView'
| 'trackUiMetric'
| 'onAddFilter'
| 'onAddBreakdownField'
| 'onAddFieldToWorkspace'
| 'onRemoveFieldFromWorkspace'
| 'additionalFilters'
Expand Down Expand Up @@ -161,6 +162,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
fullWidth,
isAffectedByGlobalFilter,
prepend,
onAddBreakdownField,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
onAddFilter,
Expand Down Expand Up @@ -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>
),
Expand All @@ -298,6 +301,7 @@ export const UnifiedFieldListSidebarComponent: React.FC<UnifiedFieldListSidebarP
alwaysShowActionButton,
compressed,
dataView,
onAddBreakdownField,
onAddFieldToWorkspace,
onRemoveFieldFromWorkspace,
onAddFilter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
import {
SEARCH_FIELDS_FROM_SOURCE,
SHOW_FIELD_STATISTICS,
Expand Down Expand Up @@ -256,6 +256,18 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {

const onFilter = isEsqlMode ? onPopulateWhereClause : onAddFilter;

const isBreakdownSupported = useMemo(
() => (isOfAggregateQueryType(query) ? !hasTransformationalCommand(query.esql) : true),
[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)) {
Expand Down Expand Up @@ -423,18 +435,19 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
sidebarToggleState$={sidebarToggleState$}
sidebarPanel={
<SidebarMemoized
additionalFilters={customFilters}
columns={currentColumns}
documents$={stateContainer.dataState.data$.documents$}
onAddBreakdownField={isBreakdownSupported ? onAddBreakdownField : undefined}
onAddField={onAddColumnWithTracking}
onRemoveField={onRemoveColumnWithTracking}
columns={currentColumns}
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={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -151,6 +155,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
selectedDataView,
columns,
trackUiMetric,
onAddBreakdownField,
onAddFilter,
onFieldEdited,
onDataViewCreated,
Expand Down Expand Up @@ -373,23 +378,24 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
<EuiFlexItem>
{selectedDataView ? (
<UnifiedFieldListSidebarContainer
ref={initializeUnifiedFieldListSidebarContainerApi}
variant={fieldListVariant}
getCreationOptions={getCreationOptions}
services={services}
dataView={selectedDataView}
trackUiMetric={trackUiMetric}
additionalFieldGroups={additionalFieldGroups}
additionalFilters={additionalFilters}
allFields={sidebarState.allFields}
showFieldList={showFieldList}
workspaceSelectedFieldNames={columns}
dataView={selectedDataView}
fullWidth
getCreationOptions={getCreationOptions}
onAddBreakdownField={onAddBreakdownField}
onAddFieldToWorkspace={onAddFieldToWorkspace}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
onAddFilter={onAddFilter}
onFieldEdited={onFieldEdited}
onRemoveFieldFromWorkspace={onRemoveFieldFromWorkspace}
prependInFlyout={prependDataViewPickerForMobile}
additionalFieldGroups={additionalFieldGroups}
additionalFilters={additionalFilters}
ref={initializeUnifiedFieldListSidebarContainerApi}
services={services}
showFieldList={showFieldList}
trackUiMetric={trackUiMetric}
variant={fieldListVariant}
workspaceSelectedFieldNames={columns}
/>
) : null}
</EuiFlexItem>
Expand Down
Loading

0 comments on commit 3c51e10

Please sign in to comment.