mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
763b5deafd
commit
73694426f9
24 changed files with 313 additions and 68 deletions
|
@ -31,6 +31,7 @@ export {
|
|||
getQueryColumnsFromESQLQuery,
|
||||
isESQLColumnSortable,
|
||||
isESQLColumnGroupable,
|
||||
isESQLFieldGroupable,
|
||||
TextBasedLanguages,
|
||||
} from './src';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
|
@ -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 () => {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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))) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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/**/*"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue