Skip to content

Commit

Permalink
[Discover] Cell actions extension (elastic#190754)
Browse files Browse the repository at this point in the history
## Summary

This PR adds a new cell actions extension to Discover using the
[`kbn-cell-actions`](packages/kbn-cell-actions) framework which Unified
Data Table already supports, allowing profiles to register additional
cell actions within the data grid:
<img width="2168" alt="cell_actions"
src="https://github.com/user-attachments/assets/f01a97be-d90f-4284-9cbf-4a2e1a5dd78c">

The extension point supports the following:
- Cell actions can be registered at the root or data source level.
- Supports an `isCompatible` method, allowing cell actions to be shown
for all cells in a column or conditionally based on the column field,
etc.
- Cell actions have access to a `context` object including the current
`field`, `value`, `dataSource`, `dataView`, `query`, `filters`, and
`timeRange`.


**Note that currently cell actions do not have access to the entire
record, only the current cell value. We can support this as a followup
if needed, but it will require an enhancement to `kbn-cell-actions`.**

## Testing
- Add `discover.experimental.enabledProfiles: ['example-root-profile',
'example-data-source-profile', 'example-document-profile']` to
`kibana.dev.yml` and start Kibana.
- Ingest the Discover context awareness example data using the following
command: `node scripts/es_archiver
--kibana-url=http://elastic:changeme@localhost:5601
--es-url=http://elastic:changeme@localhost:9200 load
test/functional/fixtures/es_archiver/discover/context_awareness`.
- Navigate to Discover and create a `my-example-logs` data view or
target the index in an ES|QL query.
- Confirm that the example cell actions appear in expanded cell popover
menus and are functional.

Resolves elastic#186576.

### Checklist

- [ ] 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)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [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
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] 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))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] 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))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
  • Loading branch information
davismcphee authored Sep 11, 2024
1 parent 66af356 commit 034625a
Show file tree
Hide file tree
Showing 28 changed files with 1,105 additions and 49 deletions.
35 changes: 21 additions & 14 deletions packages/kbn-unified-data-table/src/components/data_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,6 @@ export interface UnifiedDataTableProps {
* Callback to execute on edit runtime field
*/
onFieldEdited?: () => void;
/**
* Optional triggerId to retrieve the column cell actions that will override the default ones
*/
cellActionsTriggerId?: string;
/**
* Service dependencies
*/
Expand Down Expand Up @@ -353,6 +349,20 @@ export interface UnifiedDataTableProps {
* @param gridProps
*/
renderCustomToolbar?: UnifiedDataTableRenderCustomToolbar;
/**
* Optional triggerId to retrieve the column cell actions that will override the default ones
*/
cellActionsTriggerId?: string;
/**
* Custom set of properties used by some actions.
* An action might require a specific set of metadata properties to render.
* This data is sent directly to actions.
*/
cellActionsMetadata?: Record<string, unknown>;
/**
* Controls whether the cell actions should replace the default cell actions or be appended to them
*/
cellActionsHandling?: 'replace' | 'append';
/**
* An optional value for a custom number of the visible cell actions in the table. By default is up to 3.
**/
Expand Down Expand Up @@ -389,12 +399,6 @@ export interface UnifiedDataTableProps {
* Set to true to allow users to compare selected documents
*/
enableComparisonMode?: boolean;
/**
* Custom set of properties used by some actions.
* An action might require a specific set of metadata properties to render.
* This data is sent directly to actions.
*/
cellActionsMetadata?: Record<string, unknown>;
/**
* Optional extra props passed to the renderCellValue function/component.
*/
Expand Down Expand Up @@ -441,6 +445,9 @@ export const UnifiedDataTable = ({
isSortEnabled = true,
isPaginationEnabled = true,
cellActionsTriggerId,
cellActionsMetadata,
cellActionsHandling = 'replace',
visibleCellActions,
className,
rowHeightState,
onUpdateRowHeight,
Expand All @@ -466,14 +473,12 @@ export const UnifiedDataTable = ({
maxDocFieldsDisplayed = 50,
externalAdditionalControls,
rowsPerPageOptions,
visibleCellActions,
externalCustomRenderers,
additionalFieldGroups,
consumer = 'discover',
componentsTourSteps,
gridStyleOverride,
rowLineHeightOverride,
cellActionsMetadata,
customGridColumnsConfiguration,
enableComparisonMode,
cellContext,
Expand Down Expand Up @@ -752,7 +757,7 @@ export const UnifiedDataTable = ({

const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
() =>
cellActionsTriggerId && !isPlainRecord
cellActionsTriggerId
? visibleColumns.map(
(columnName) =>
dataView.getFieldByName(columnName)?.toSpec() ?? {
Expand All @@ -763,7 +768,7 @@ export const UnifiedDataTable = ({
}
)
: undefined,
[cellActionsTriggerId, isPlainRecord, visibleColumns, dataView]
[cellActionsTriggerId, visibleColumns, dataView]
);
const allCellActionsMetadata = useMemo(
() => ({ dataViewId: dataView.id, ...(cellActionsMetadata ?? {}) }),
Expand Down Expand Up @@ -806,6 +811,7 @@ export const UnifiedDataTable = ({
getEuiGridColumns({
columns: visibleColumns,
columnsCellActions,
cellActionsHandling,
rowsCount: displayedRows.length,
settings,
dataView,
Expand All @@ -829,6 +835,7 @@ export const UnifiedDataTable = ({
onResize,
}),
[
cellActionsHandling,
columnsMeta,
columnsCellActions,
customGridColumnsConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('Data table columns', function () {
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
cellActionsHandling: 'replace',
});
expect(actual).toMatchSnapshot();
});
Expand All @@ -75,6 +76,7 @@ describe('Data table columns', function () {
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
cellActionsHandling: 'replace',
});
expect(actual).toMatchSnapshot();
});
Expand Down Expand Up @@ -103,9 +105,79 @@ describe('Data table columns', function () {
timestamp: { type: 'date', esType: 'dateTime' },
},
onResize: () => {},
cellActionsHandling: 'replace',
});
expect(actual).toMatchSnapshot();
});

describe('cell actions', () => {
it('should replace cell actions', async () => {
const cellAction = jest.fn();
const actual = getEuiGridColumns({
columns: columnsWithTimeCol,
settings: {},
dataView: dataViewWithTimefieldMock,
defaultColumns: false,
isSortEnabled: true,
isPlainRecord: true,
valueToStringConverter: dataTableContextMock.valueToStringConverter,
rowsCount: 100,
headerRowHeightLines: 5,
services: {
uiSettings: servicesMock.uiSettings,
toastNotifications: servicesMock.toastNotifications,
},
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
columnsMeta: {
extension: { type: 'string' },
message: { type: 'string', esType: 'keyword' },
timestamp: { type: 'date', esType: 'dateTime' },
},
onResize: () => {},
columnsCellActions: [[cellAction]],
cellActionsHandling: 'replace',
});
expect(actual[0].cellActions).toEqual([cellAction]);
});

it('should append cell actions', async () => {
const cellAction = jest.fn();
const actual = getEuiGridColumns({
columns: columnsWithTimeCol,
settings: {},
dataView: dataViewWithTimefieldMock,
defaultColumns: false,
isSortEnabled: true,
isPlainRecord: true,
valueToStringConverter: dataTableContextMock.valueToStringConverter,
rowsCount: 100,
headerRowHeightLines: 5,
services: {
uiSettings: servicesMock.uiSettings,
toastNotifications: servicesMock.toastNotifications,
},
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
columnsMeta: {
extension: { type: 'string' },
message: { type: 'string', esType: 'keyword' },
timestamp: { type: 'date', esType: 'dateTime' },
},
onResize: () => {},
columnsCellActions: [[cellAction]],
cellActionsHandling: 'append',
});
expect(actual[0].cellActions).toEqual([
expect.any(Function),
expect.any(Function),
expect.any(Function),
cellAction,
]);
});
});
});

describe('getVisibleColumns', () => {
Expand Down Expand Up @@ -302,6 +374,7 @@ describe('Data table columns', function () {
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
cellActionsHandling: 'replace',
});
expect(actual).toMatchSnapshot();
});
Expand Down Expand Up @@ -330,6 +403,7 @@ describe('Data table columns', function () {
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onFilter: () => {},
onResize: () => {},
cellActionsHandling: 'replace',
});
expect(actual).toMatchSnapshot();
});
Expand Down Expand Up @@ -363,6 +437,7 @@ describe('Data table columns', function () {
extension: { type: 'string' },
},
onResize: () => {},
cellActionsHandling: 'replace',
});
expect(gridColumns[1].schema).toBe('string');
});
Expand Down Expand Up @@ -394,6 +469,7 @@ describe('Data table columns', function () {
var_test: { type: 'number' },
},
onResize: () => {},
cellActionsHandling: 'replace',
});
expect(gridColumns[1].schema).toBe('numeric');
});
Expand Down Expand Up @@ -421,6 +497,7 @@ describe('Data table columns', function () {
message: { type: 'string', esType: 'keyword' },
},
onResize: () => {},
cellActionsHandling: 'replace',
});

const extensionGridColumn = gridColumns[0];
Expand Down Expand Up @@ -452,6 +529,7 @@ describe('Data table columns', function () {
message: { type: 'string', esType: 'keyword' },
},
onResize: () => {},
cellActionsHandling: 'replace',
});

expect(customizedGridColumns).toMatchSnapshot();
Expand Down Expand Up @@ -495,6 +573,7 @@ describe('Data table columns', function () {
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
onResize: () => {},
cellActionsHandling: 'replace',
});
const columnDisplayNames = customizedGridColumns.map((column) => column.displayAsText);
expect(columnDisplayNames.includes('test_column_one')).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ function buildEuiGridColumn({
onFilter,
editField,
columnCellActions,
cellActionsHandling,
visibleCellActions,
columnsMeta,
showColumnTokens,
Expand All @@ -124,6 +125,7 @@ function buildEuiGridColumn({
onFilter?: DocViewFilterFn;
editField?: (fieldName: string) => void;
columnCellActions?: EuiDataGridColumnCellAction[];
cellActionsHandling: 'replace' | 'append';
visibleCellActions?: number;
columnsMeta?: DataTableColumnsMeta;
showColumnTokens?: boolean;
Expand Down Expand Up @@ -176,12 +178,16 @@ function buildEuiGridColumn({

let cellActions: EuiDataGridColumnCellAction[];

if (columnCellActions?.length) {
if (columnCellActions?.length && cellActionsHandling === 'replace') {
cellActions = columnCellActions;
} else {
cellActions = dataViewField
? buildCellActions(dataViewField, toastNotifications, valueToStringConverter, onFilter)
: [];

if (columnCellActions?.length && cellActionsHandling === 'append') {
cellActions.push(...columnCellActions);
}
}

const columnType = columnsMeta?.[columnName]?.type ?? dataViewField?.type;
Expand Down Expand Up @@ -278,6 +284,7 @@ export const deserializeHeaderRowHeight = (headerRowHeightLines: number) => {
export function getEuiGridColumns({
columns,
columnsCellActions,
cellActionsHandling,
rowsCount,
settings,
dataView,
Expand All @@ -298,6 +305,7 @@ export function getEuiGridColumns({
}: {
columns: string[];
columnsCellActions?: EuiDataGridColumnCellAction[][];
cellActionsHandling: 'replace' | 'append';
rowsCount: number;
settings: UnifiedDataTableSettings | undefined;
dataView: DataView;
Expand Down Expand Up @@ -328,6 +336,7 @@ export function getEuiGridColumns({
numberOfColumns,
columnName: column,
columnCellActions: columnsCellActions?.[columnIndex],
cellActionsHandling,
columnWidth: getColWidth(column),
dataView,
defaultColumns,
Expand Down
35 changes: 35 additions & 0 deletions src/plugins/discover/common/data_sources/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { DataView } from '@kbn/data-views-plugin/common';
import { dataViewWithTimefieldMock } from '../../public/__mocks__/data_view_with_timefield';
import { createDataSource, createDataViewDataSource, createEsqlDataSource } from './utils';

describe('createDataSource', () => {
it('should return ES|QL source when ES|QL query', () => {
const dataView = dataViewWithTimefieldMock;
const query = { esql: 'FROM *' };
const result = createDataSource({ dataView, query });
expect(result).toEqual(createEsqlDataSource());
});

it('should return data view source when not ES|QL query and dataView id is defined', () => {
const dataView = dataViewWithTimefieldMock;
const query = { language: 'kql', query: 'test' };
const result = createDataSource({ dataView, query });
expect(result).toEqual(createDataViewDataSource({ dataViewId: dataView.id! }));
});

it('should return undefined when not ES|QL query and dataView id is not defined', () => {
const dataView = { ...dataViewWithTimefieldMock, id: undefined } as DataView;
const query = { language: 'kql', query: 'test' };
const result = createDataSource({ dataView, query });
expect(result).toEqual(undefined);
});
});
23 changes: 22 additions & 1 deletion src/plugins/discover/common/data_sources/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { DataSourceType, DataViewDataSource, DiscoverDataSource, EsqlDataSource } from './types';
import { isOfAggregateQueryType, type AggregateQuery, type Query } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/common';
import {
DataSourceType,
type DataViewDataSource,
type DiscoverDataSource,
type EsqlDataSource,
} from './types';

export const createDataViewDataSource = ({
dataViewId,
Expand All @@ -22,6 +29,20 @@ export const createEsqlDataSource = (): EsqlDataSource => ({
type: DataSourceType.Esql,
});

export const createDataSource = ({
dataView,
query,
}: {
dataView: DataView | undefined;
query: Query | AggregateQuery | undefined;
}) => {
return isOfAggregateQueryType(query)
? createEsqlDataSource()
: dataView?.id
? createDataViewDataSource({ dataViewId: dataView.id })
: undefined;
};

export const isDataSourceType = <T extends DataSourceType>(
dataSource: DiscoverDataSource | undefined,
type: T
Expand Down
Loading

0 comments on commit 034625a

Please sign in to comment.