Skip to content

Commit

Permalink
[ES|QL] Adds the ability to breakdown the histogram in Discover (elas…
Browse files Browse the repository at this point in the history
…tic#193820)

## Summary

Part of elastic#186369

It enables the users to breakdown the histogram visualization in
Discover.


![meow](https://github.com/user-attachments/assets/d5fdaa41-0a69-4caf-9da2-1221dcfd5ce2)


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
stratoula and kibanamachine authored Oct 1, 2024
1 parent 8776fe5 commit dfe00f2
Show file tree
Hide file tree
Showing 13 changed files with 451 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

import { render, act, screen } from '@testing-library/react';
import React from 'react';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { convertDatatableColumnToDataViewFieldSpec } from '@kbn/data-view-utils';
import { DataViewField } from '@kbn/data-views-plugin/common';
import { UnifiedHistogramBreakdownContext } from '../types';
import { dataViewWithTimefieldMock } from '../__mocks__/data_view_with_timefield';
import { BreakdownFieldSelector } from './breakdown_field_selector';

describe('BreakdownFieldSelector', () => {
it('should render correctly', () => {
it('should render correctly for dataview fields', () => {
const onBreakdownFieldChange = jest.fn();
const breakdown: UnifiedHistogramBreakdownContext = {
field: undefined,
Expand Down Expand Up @@ -63,6 +66,67 @@ describe('BreakdownFieldSelector', () => {
`);
});

it('should render correctly for ES|QL columns', () => {
const onBreakdownFieldChange = jest.fn();
const breakdown: UnifiedHistogramBreakdownContext = {
field: undefined,
};

render(
<BreakdownFieldSelector
dataView={dataViewWithTimefieldMock}
breakdown={breakdown}
onBreakdownFieldChange={onBreakdownFieldChange}
esqlColumns={[
{
name: 'bytes',
meta: { type: 'number' },
id: 'bytes',
},
{
name: 'extension',
meta: { type: 'string' },
id: 'extension',
},
]}
/>
);

const button = screen.getByTestId('unifiedHistogramBreakdownSelectorButton');
expect(button.getAttribute('data-selected-value')).toBe(null);

act(() => {
button.click();
});

const options = screen.getAllByRole('option');
expect(
options.map((option) => ({
label: option.getAttribute('title'),
value: option.getAttribute('value'),
checked: option.getAttribute('aria-checked'),
}))
).toMatchInlineSnapshot(`
Array [
Object {
"checked": "true",
"label": "No breakdown",
"value": "__EMPTY_SELECTOR_OPTION__",
},
Object {
"checked": "false",
"label": "bytes",
"value": "bytes",
},
Object {
"checked": "false",
"label": "extension",
"value": "extension",
},
]
`);
});

it('should mark the option as checked if breakdown.field is defined', () => {
const onBreakdownFieldChange = jest.fn();
const field = dataViewWithTimefieldMock.fields.find((f) => f.name === 'extension')!;
Expand Down Expand Up @@ -111,7 +175,7 @@ describe('BreakdownFieldSelector', () => {
`);
});

it('should call onBreakdownFieldChange with the selected field when the user selects a field', () => {
it('should call onBreakdownFieldChange with the selected field when the user selects a dataview field', () => {
const onBreakdownFieldChange = jest.fn();
const selectedField = dataViewWithTimefieldMock.fields.find((f) => f.name === 'bytes')!;
const breakdown: UnifiedHistogramBreakdownContext = {
Expand All @@ -135,4 +199,45 @@ describe('BreakdownFieldSelector', () => {

expect(onBreakdownFieldChange).toHaveBeenCalledWith(selectedField);
});

it('should call onBreakdownFieldChange with the selected field when the user selects an ES|QL field', () => {
const onBreakdownFieldChange = jest.fn();
const esqlColumns = [
{
name: 'bytes',
meta: { type: 'number' },
id: 'bytes',
},
{
name: 'extension',
meta: { type: 'string' },
id: 'extension',
},
] as DatatableColumn[];
const breakdownColumn = esqlColumns.find((c) => c.name === 'bytes')!;
const selectedField = new DataViewField(
convertDatatableColumnToDataViewFieldSpec(breakdownColumn)
);
const breakdown: UnifiedHistogramBreakdownContext = {
field: undefined,
};
render(
<BreakdownFieldSelector
dataView={dataViewWithTimefieldMock}
breakdown={breakdown}
onBreakdownFieldChange={onBreakdownFieldChange}
esqlColumns={esqlColumns}
/>
);

act(() => {
screen.getByTestId('unifiedHistogramBreakdownSelectorButton').click();
});

act(() => {
screen.getByTitle('bytes').click();
});

expect(onBreakdownFieldChange).toHaveBeenCalledWith(selectedField);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import React, { useCallback, useMemo } from 'react';
import { EuiSelectableOption } from '@elastic/eui';
import { FieldIcon, getFieldIconProps, comboBoxFieldOptionMatcher } from '@kbn/field-utils';
import { css } from '@emotion/react';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { type DataView, DataViewField } from '@kbn/data-views-plugin/common';
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';
Expand All @@ -25,17 +27,32 @@ import {
export interface BreakdownFieldSelectorProps {
dataView: DataView;
breakdown: UnifiedHistogramBreakdownContext;
esqlColumns?: DatatableColumn[];
onBreakdownFieldChange?: (breakdownField: DataViewField | undefined) => void;
}

const mapToDropdownFields = (dataView: DataView, esqlColumns?: DatatableColumn[]) => {
if (esqlColumns) {
return (
esqlColumns
.map((column) => new DataViewField(convertDatatableColumnToDataViewFieldSpec(column)))
// filter out unsupported field types
.filter((field) => field.type !== 'unknown')
);
}

return dataView.fields.filter(fieldSupportsBreakdown);
};

export const BreakdownFieldSelector = ({
dataView,
breakdown,
esqlColumns,
onBreakdownFieldChange,
}: BreakdownFieldSelectorProps) => {
const fields = useMemo(() => mapToDropdownFields(dataView, esqlColumns), [dataView, esqlColumns]);
const fieldOptions: SelectableEntry[] = useMemo(() => {
const options: SelectableEntry[] = dataView.fields
.filter(fieldSupportsBreakdown)
const options: SelectableEntry[] = fields
.map((field) => ({
key: field.name,
name: field.name,
Expand Down Expand Up @@ -69,16 +86,16 @@ export const BreakdownFieldSelector = ({
});

return options;
}, [dataView, breakdown.field]);
}, [fields, breakdown?.field]);

const onChange = useCallback<NonNullable<ToolbarSelectorProps['onChange']>>(
(chosenOption) => {
const field = chosenOption?.value
? dataView.fields.find((currentField) => currentField.name === chosenOption.value)
const breakdownField = chosenOption?.value
? fields.find((currentField) => currentField.name === chosenOption.value)
: undefined;
onBreakdownFieldChange?.(field);
onBreakdownFieldChange?.(breakdownField);
},
[dataView.fields, onBreakdownFieldChange]
[fields, onBreakdownFieldChange]
);

return (
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/unified_histogram/public/chart/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
LensEmbeddableInput,
LensEmbeddableOutput,
} from '@kbn/lens-plugin/public';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { TimeRange } from '@kbn/es-query';
import { Histogram } from './histogram';
Expand Down Expand Up @@ -79,6 +80,7 @@ export interface ChartProps {
onFilter?: LensEmbeddableInput['onFilter'];
onBrushEnd?: LensEmbeddableInput['onBrushEnd'];
withDefaultActions: EmbeddableComponentProps['withDefaultActions'];
columns?: DatatableColumn[];
}

const HistogramMemoized = memo(Histogram);
Expand Down Expand Up @@ -114,6 +116,7 @@ export function Chart({
onBrushEnd,
withDefaultActions,
abortController,
columns,
}: ChartProps) {
const lensVisServiceCurrentSuggestionContext = useObservable(
lensVisService.currentSuggestionContext$
Expand Down Expand Up @@ -312,6 +315,7 @@ export function Chart({
dataView={dataView}
breakdown={breakdown}
onBreakdownFieldChange={onBreakdownFieldChange}
esqlColumns={isPlainRecord ? columns : undefined}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export const UnifiedHistogramContainer = forwardRef<
query,
searchSessionId,
requestAdapter,
columns: containerProps.columns,
});

const handleVisContextChange: UnifiedHistogramLayoutProps['onVisContextChanged'] | undefined =
Expand Down
Loading

0 comments on commit dfe00f2

Please sign in to comment.