mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Discover] Cell actions extension (#190754)
## 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 #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)
This commit is contained in:
parent
66af356731
commit
034625a70b
28 changed files with 1105 additions and 49 deletions
|
@ -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
|
||||
*/
|
||||
|
@ -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.
|
||||
**/
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -441,6 +445,9 @@ export const UnifiedDataTable = ({
|
|||
isSortEnabled = true,
|
||||
isPaginationEnabled = true,
|
||||
cellActionsTriggerId,
|
||||
cellActionsMetadata,
|
||||
cellActionsHandling = 'replace',
|
||||
visibleCellActions,
|
||||
className,
|
||||
rowHeightState,
|
||||
onUpdateRowHeight,
|
||||
|
@ -466,14 +473,12 @@ export const UnifiedDataTable = ({
|
|||
maxDocFieldsDisplayed = 50,
|
||||
externalAdditionalControls,
|
||||
rowsPerPageOptions,
|
||||
visibleCellActions,
|
||||
externalCustomRenderers,
|
||||
additionalFieldGroups,
|
||||
consumer = 'discover',
|
||||
componentsTourSteps,
|
||||
gridStyleOverride,
|
||||
rowLineHeightOverride,
|
||||
cellActionsMetadata,
|
||||
customGridColumnsConfiguration,
|
||||
enableComparisonMode,
|
||||
cellContext,
|
||||
|
@ -752,7 +757,7 @@ export const UnifiedDataTable = ({
|
|||
|
||||
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
|
||||
() =>
|
||||
cellActionsTriggerId && !isPlainRecord
|
||||
cellActionsTriggerId
|
||||
? visibleColumns.map(
|
||||
(columnName) =>
|
||||
dataView.getFieldByName(columnName)?.toSpec() ?? {
|
||||
|
@ -763,7 +768,7 @@ export const UnifiedDataTable = ({
|
|||
}
|
||||
)
|
||||
: undefined,
|
||||
[cellActionsTriggerId, isPlainRecord, visibleColumns, dataView]
|
||||
[cellActionsTriggerId, visibleColumns, dataView]
|
||||
);
|
||||
const allCellActionsMetadata = useMemo(
|
||||
() => ({ dataViewId: dataView.id, ...(cellActionsMetadata ?? {}) }),
|
||||
|
@ -806,6 +811,7 @@ export const UnifiedDataTable = ({
|
|||
getEuiGridColumns({
|
||||
columns: visibleColumns,
|
||||
columnsCellActions,
|
||||
cellActionsHandling,
|
||||
rowsCount: displayedRows.length,
|
||||
settings,
|
||||
dataView,
|
||||
|
@ -829,6 +835,7 @@ export const UnifiedDataTable = ({
|
|||
onResize,
|
||||
}),
|
||||
[
|
||||
cellActionsHandling,
|
||||
columnsMeta,
|
||||
columnsCellActions,
|
||||
customGridColumnsConfiguration,
|
||||
|
|
|
@ -52,6 +52,7 @@ describe('Data table columns', function () {
|
|||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
@ -75,6 +76,7 @@ describe('Data table columns', function () {
|
|||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
@ -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', () => {
|
||||
|
@ -302,6 +374,7 @@ describe('Data table columns', function () {
|
|||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
@ -330,6 +403,7 @@ describe('Data table columns', function () {
|
|||
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
@ -363,6 +437,7 @@ describe('Data table columns', function () {
|
|||
extension: { type: 'string' },
|
||||
},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
expect(gridColumns[1].schema).toBe('string');
|
||||
});
|
||||
|
@ -394,6 +469,7 @@ describe('Data table columns', function () {
|
|||
var_test: { type: 'number' },
|
||||
},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
expect(gridColumns[1].schema).toBe('numeric');
|
||||
});
|
||||
|
@ -421,6 +497,7 @@ describe('Data table columns', function () {
|
|||
message: { type: 'string', esType: 'keyword' },
|
||||
},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
|
||||
const extensionGridColumn = gridColumns[0];
|
||||
|
@ -452,6 +529,7 @@ describe('Data table columns', function () {
|
|||
message: { type: 'string', esType: 'keyword' },
|
||||
},
|
||||
onResize: () => {},
|
||||
cellActionsHandling: 'replace',
|
||||
});
|
||||
|
||||
expect(customizedGridColumns).toMatchSnapshot();
|
||||
|
@ -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();
|
||||
|
|
|
@ -102,6 +102,7 @@ function buildEuiGridColumn({
|
|||
onFilter,
|
||||
editField,
|
||||
columnCellActions,
|
||||
cellActionsHandling,
|
||||
visibleCellActions,
|
||||
columnsMeta,
|
||||
showColumnTokens,
|
||||
|
@ -124,6 +125,7 @@ function buildEuiGridColumn({
|
|||
onFilter?: DocViewFilterFn;
|
||||
editField?: (fieldName: string) => void;
|
||||
columnCellActions?: EuiDataGridColumnCellAction[];
|
||||
cellActionsHandling: 'replace' | 'append';
|
||||
visibleCellActions?: number;
|
||||
columnsMeta?: DataTableColumnsMeta;
|
||||
showColumnTokens?: boolean;
|
||||
|
@ -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;
|
||||
|
@ -278,6 +284,7 @@ export const deserializeHeaderRowHeight = (headerRowHeightLines: number) => {
|
|||
export function getEuiGridColumns({
|
||||
columns,
|
||||
columnsCellActions,
|
||||
cellActionsHandling,
|
||||
rowsCount,
|
||||
settings,
|
||||
dataView,
|
||||
|
@ -298,6 +305,7 @@ export function getEuiGridColumns({
|
|||
}: {
|
||||
columns: string[];
|
||||
columnsCellActions?: EuiDataGridColumnCellAction[][];
|
||||
cellActionsHandling: 'replace' | 'append';
|
||||
rowsCount: number;
|
||||
settings: UnifiedDataTableSettings | undefined;
|
||||
dataView: DataView;
|
||||
|
@ -328,6 +336,7 @@ export function getEuiGridColumns({
|
|||
numberOfColumns,
|
||||
columnName: column,
|
||||
columnCellActions: columnsCellActions?.[columnIndex],
|
||||
cellActionsHandling,
|
||||
columnWidth: getColWidth(column),
|
||||
dataView,
|
||||
defaultColumns,
|
||||
|
|
35
src/plugins/discover/common/data_sources/utils.test.ts
Normal file
35
src/plugins/discover/common/data_sources/utils.test.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
@ -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
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { fieldList } from '@kbn/data-views-plugin/common';
|
||||
import { FieldSpec } from '@kbn/data-views-plugin/public';
|
||||
import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
|
||||
const fields = [
|
||||
|
@ -16,6 +17,7 @@ const fields = [
|
|||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
|
@ -25,6 +27,7 @@ const fields = [
|
|||
filterable: true,
|
||||
aggregatable: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
|
@ -32,6 +35,7 @@ const fields = [
|
|||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'extension',
|
||||
|
@ -40,6 +44,7 @@ const fields = [
|
|||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
|
@ -48,6 +53,7 @@ const fields = [
|
|||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'scripted',
|
||||
|
@ -56,10 +62,10 @@ const fields = [
|
|||
scripted: true,
|
||||
filterable: false,
|
||||
},
|
||||
] as DataView['fields'];
|
||||
];
|
||||
|
||||
export const dataViewWithTimefieldMock = buildDataViewMock({
|
||||
name: 'index-pattern-with-timefield',
|
||||
fields,
|
||||
fields: fieldList(fields as unknown as FieldSpec[]),
|
||||
timeFieldName: 'timestamp',
|
||||
});
|
||||
|
|
|
@ -148,6 +148,7 @@ export function createDiscoverServicesMock(): DiscoverServices {
|
|||
corePluginMock.chrome.getActiveSolutionNavId$.mockReturnValue(new BehaviorSubject(null));
|
||||
|
||||
return {
|
||||
application: corePluginMock.application,
|
||||
core: corePluginMock,
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
chrome: chromeServiceMock.createStartContract(),
|
||||
|
|
|
@ -22,7 +22,6 @@ import { uiSettingsMock } from '../../__mocks__/ui_settings';
|
|||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { LocalStorageMock } from '../../__mocks__/local_storage_mock';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import type { HistoryLocationState } from '../../build_services';
|
||||
import { createSearchSessionMock } from '../../__mocks__/search_session';
|
||||
import { createDiscoverServicesMock } from '../../__mocks__/services';
|
||||
|
@ -36,14 +35,7 @@ const discoverServices = createDiscoverServicesMock();
|
|||
describe('ContextApp test', () => {
|
||||
const { history } = createSearchSessionMock();
|
||||
const services = {
|
||||
data: {
|
||||
...dataPluginMock.createStartContract(),
|
||||
search: {
|
||||
searchSource: {
|
||||
createEmpty: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
data: discoverServices.data,
|
||||
capabilities: {
|
||||
discover: {
|
||||
save: true,
|
||||
|
@ -80,6 +72,8 @@ describe('ContextApp test', () => {
|
|||
contextLocator: { getRedirectUrl: jest.fn(() => '') },
|
||||
singleDocLocator: { getRedirectUrl: jest.fn(() => '') },
|
||||
profilesManager: discoverServices.profilesManager,
|
||||
timefilter: discoverServices.timefilter,
|
||||
uiActions: discoverServices.uiActions,
|
||||
} as unknown as DiscoverServices;
|
||||
|
||||
const defaultProps = {
|
||||
|
|
|
@ -16,12 +16,17 @@ import { SortDirection } from '@kbn/data-plugin/public';
|
|||
import { UnifiedDataTable } from '@kbn/unified-data-table';
|
||||
import { ContextAppContent, ContextAppContentProps } from './context_app_content';
|
||||
import { LoadingStatus } from './services/context_query_state';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
import { DocTableWrapper } from '../../components/doc_table/doc_table_wrapper';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { buildDataTableRecord } from '@kbn/discover-utils';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { buildDataViewMock, deepMockedFields } from '@kbn/discover-utils/src/__mocks__';
|
||||
|
||||
const dataViewMock = buildDataViewMock({
|
||||
name: 'the-data-view',
|
||||
fields: deepMockedFields,
|
||||
});
|
||||
|
||||
describe('ContextAppContent test', () => {
|
||||
const mountComponent = async ({
|
||||
|
|
|
@ -30,6 +30,9 @@ import {
|
|||
} from '@kbn/discover-utils';
|
||||
import { DataLoadingState, UnifiedDataTableProps } from '@kbn/unified-data-table';
|
||||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { map } from 'rxjs';
|
||||
import { DiscoverGrid } from '../../components/discover_grid';
|
||||
import { getDefaultRowsPerPage } from '../../../common/constants';
|
||||
import { LoadingStatus } from './services/context_query_state';
|
||||
|
@ -41,7 +44,12 @@ import { DocTableContext } from '../../components/doc_table/doc_table_context';
|
|||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
import { DiscoverGridFlyout } from '../../components/discover_grid_flyout';
|
||||
import { onResizeGridColumn } from '../../utils/on_resize_grid_column';
|
||||
import { useProfileAccessor } from '../../context_awareness';
|
||||
import {
|
||||
DISCOVER_CELL_ACTIONS_TRIGGER,
|
||||
useAdditionalCellActions,
|
||||
useProfileAccessor,
|
||||
} from '../../context_awareness';
|
||||
import { createDataSource } from '../../../common/data_sources';
|
||||
|
||||
export interface ContextAppContentProps {
|
||||
columns: string[];
|
||||
|
@ -132,6 +140,7 @@ export function ContextAppContent({
|
|||
},
|
||||
[setAppState]
|
||||
);
|
||||
|
||||
const sort = useMemo(() => {
|
||||
return [[dataView.timeFieldName!, SortDirection.desc]];
|
||||
}, [dataView]);
|
||||
|
@ -167,6 +176,21 @@ export function ContextAppContent({
|
|||
return getCellRenderers();
|
||||
}, [getCellRenderersAccessor]);
|
||||
|
||||
const dataSource = useMemo(() => createDataSource({ dataView, query: undefined }), [dataView]);
|
||||
const { filters } = useQuerySubscriber({ data: services.data });
|
||||
const timeRange = useObservable(
|
||||
services.timefilter.getTimeUpdate$().pipe(map(() => services.timefilter.getTime())),
|
||||
services.timefilter.getTime()
|
||||
);
|
||||
|
||||
const cellActionsMetadata = useAdditionalCellActions({
|
||||
dataSource,
|
||||
dataView,
|
||||
query: undefined,
|
||||
filters,
|
||||
timeRange,
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<WrapperWithPadding>
|
||||
|
@ -206,6 +230,9 @@ export function ContextAppContent({
|
|||
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
|
||||
<DiscoverGridMemoized
|
||||
ariaLabelledBy="surDocumentsAriaLabel"
|
||||
cellActionsTriggerId={DISCOVER_CELL_ACTIONS_TRIGGER.id}
|
||||
cellActionsMetadata={cellActionsMetadata}
|
||||
cellActionsHandling="append"
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
dataView={dataView}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { buildDataTableRecord } from '@kbn/discover-utils';
|
|||
import type { DataTableRecord, EsHitRecord } from '@kbn/discover-utils/types';
|
||||
import type { SearchResponseWarning } from '@kbn/search-response-warnings';
|
||||
import type { DiscoverServices } from '../../../build_services';
|
||||
import { createDataViewDataSource } from '../../../../common/data_sources';
|
||||
import { createDataSource } from '../../../../common/data_sources';
|
||||
|
||||
export async function fetchAnchor(
|
||||
anchorId: string,
|
||||
|
@ -34,7 +34,7 @@ export async function fetchAnchor(
|
|||
const solutionNavId = await firstValueFrom(core.chrome.getActiveSolutionNavId$());
|
||||
await profilesManager.resolveRootProfile({ solutionNavId });
|
||||
await profilesManager.resolveDataSourceProfile({
|
||||
dataSource: dataView?.id ? createDataViewDataSource({ dataViewId: dataView.id }) : undefined,
|
||||
dataSource: createDataSource({ dataView, query: undefined }),
|
||||
dataView,
|
||||
query: { query: '', language: 'kuery' },
|
||||
});
|
||||
|
|
|
@ -46,6 +46,8 @@ import {
|
|||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
|
||||
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
||||
import { map } from 'rxjs';
|
||||
import { DiscoverGrid } from '../../../../components/discover_grid';
|
||||
import { getDefaultRowsPerPage } from '../../../../../common/constants';
|
||||
import { useInternalStateSelector } from '../../state_management/discover_internal_state_container';
|
||||
|
@ -74,7 +76,11 @@ import { onResizeGridColumn } from '../../../../utils/on_resize_grid_column';
|
|||
import { useContextualGridCustomisations } from '../../hooks/grid_customisations';
|
||||
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
|
||||
import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups';
|
||||
import { useProfileAccessor } from '../../../../context_awareness';
|
||||
import {
|
||||
DISCOVER_CELL_ACTIONS_TRIGGER,
|
||||
useAdditionalCellActions,
|
||||
useProfileAccessor,
|
||||
} from '../../../../context_awareness';
|
||||
|
||||
const containerStyles = css`
|
||||
position: relative;
|
||||
|
@ -117,6 +123,7 @@ function DiscoverDocumentsComponent({
|
|||
const savedSearch = useSavedSearchInitial();
|
||||
const { dataViews, capabilities, uiSettings, uiActions } = services;
|
||||
const [
|
||||
dataSource,
|
||||
query,
|
||||
sort,
|
||||
rowHeight,
|
||||
|
@ -128,6 +135,7 @@ function DiscoverDocumentsComponent({
|
|||
density,
|
||||
] = useAppStateSelector((state) => {
|
||||
return [
|
||||
state.dataSource,
|
||||
state.query,
|
||||
state.sort,
|
||||
state.rowHeight,
|
||||
|
@ -264,6 +272,21 @@ function DiscoverDocumentsComponent({
|
|||
[documentState.esqlQueryColumns]
|
||||
);
|
||||
|
||||
const { filters } = useQuerySubscriber({ data: services.data });
|
||||
|
||||
const timeRange = useObservable(
|
||||
services.timefilter.getTimeUpdate$().pipe(map(() => services.timefilter.getTime())),
|
||||
services.timefilter.getTime()
|
||||
);
|
||||
|
||||
const cellActionsMetadata = useAdditionalCellActions({
|
||||
dataSource,
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
timeRange,
|
||||
});
|
||||
|
||||
const renderDocumentView = useCallback(
|
||||
(
|
||||
hit: DataTableRecord,
|
||||
|
@ -470,6 +493,9 @@ function DiscoverDocumentsComponent({
|
|||
additionalFieldGroups={additionalFieldGroups}
|
||||
dataGridDensityState={density}
|
||||
onUpdateDataGridDensity={onUpdateDensity}
|
||||
cellActionsTriggerId={DISCOVER_CELL_ACTIONS_TRIGGER.id}
|
||||
cellActionsMetadata={cellActionsMetadata}
|
||||
cellActionsHandling="append"
|
||||
/>
|
||||
</CellActionsProvider>
|
||||
</div>
|
||||
|
|
|
@ -34,6 +34,18 @@ export const createContextAwarenessMocks = ({
|
|||
...prev(),
|
||||
rootProfile: () => <>root-profile</>,
|
||||
})),
|
||||
getAdditionalCellActions: jest.fn((prev) => () => [
|
||||
...prev(),
|
||||
{
|
||||
id: 'root-action',
|
||||
getDisplayName: () => 'Root action',
|
||||
getIconType: () => 'minus',
|
||||
isCompatible: () => false,
|
||||
execute: () => {
|
||||
alert('Root action executed');
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
resolve: jest.fn(() => ({
|
||||
isMatch: true,
|
||||
|
@ -71,6 +83,17 @@ export const createContextAwarenessMocks = ({
|
|||
],
|
||||
rowHeight: 3,
|
||||
})),
|
||||
getAdditionalCellActions: jest.fn((prev) => () => [
|
||||
...prev(),
|
||||
{
|
||||
id: 'data-source-action',
|
||||
getDisplayName: () => 'Data source action',
|
||||
getIconType: () => 'plus',
|
||||
execute: () => {
|
||||
alert('Data source action executed');
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
resolve: jest.fn(() => ({
|
||||
isMatch: true,
|
||||
|
|
|
@ -9,3 +9,4 @@
|
|||
|
||||
export { useProfileAccessor } from './use_profile_accessor';
|
||||
export { useRootProfile } from './use_root_profile';
|
||||
export { useAdditionalCellActions } from './use_additional_cell_actions';
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
import {
|
||||
DISCOVER_CELL_ACTION_TYPE,
|
||||
createCellAction,
|
||||
toCellActionContext,
|
||||
useAdditionalCellActions,
|
||||
} from './use_additional_cell_actions';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
import React from 'react';
|
||||
import { createEsqlDataSource } from '../../../common/data_sources';
|
||||
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
|
||||
import type {
|
||||
Action,
|
||||
ActionDefinition,
|
||||
ActionExecutionContext,
|
||||
} from '@kbn/ui-actions-plugin/public/actions';
|
||||
import {
|
||||
DISCOVER_CELL_ACTIONS_TRIGGER,
|
||||
type AdditionalCellAction,
|
||||
type DiscoverCellActionExecutionContext,
|
||||
} from '../types';
|
||||
import { createContextAwarenessMocks } from '../__mocks__';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
|
||||
let mockUuid = 0;
|
||||
|
||||
jest.mock('uuid', () => ({ ...jest.requireActual('uuid'), v4: () => (++mockUuid).toString() }));
|
||||
|
||||
const mockActions: Array<ActionDefinition<DiscoverCellActionExecutionContext>> = [];
|
||||
const mockTriggerActions: Record<string, string[]> = { [DISCOVER_CELL_ACTIONS_TRIGGER.id]: [] };
|
||||
|
||||
jest.spyOn(discoverServiceMock.uiActions, 'registerAction').mockImplementation((action) => {
|
||||
mockActions.push(action as ActionDefinition<DiscoverCellActionExecutionContext>);
|
||||
return action as Action;
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(discoverServiceMock.uiActions, 'attachAction')
|
||||
.mockImplementation((triggerId, actionId) => {
|
||||
mockTriggerActions[triggerId].push(actionId);
|
||||
});
|
||||
|
||||
jest.spyOn(discoverServiceMock.uiActions, 'unregisterAction').mockImplementation((id) => {
|
||||
mockActions.splice(
|
||||
mockActions.findIndex((action) => action.id === id),
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(discoverServiceMock.uiActions, 'detachAction')
|
||||
.mockImplementation((triggerId, actionId) => {
|
||||
mockTriggerActions[triggerId].splice(
|
||||
mockTriggerActions[triggerId].findIndex((action) => action === actionId),
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
describe('useAdditionalCellActions', () => {
|
||||
const initialProps: Parameters<typeof useAdditionalCellActions>[0] = {
|
||||
dataSource: createEsqlDataSource(),
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
query: { esql: `FROM ${dataViewWithTimefieldMock.getIndexPattern()}` },
|
||||
filters: [],
|
||||
timeRange: { from: 'now-15m', to: 'now' },
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
return renderHook((props) => useAdditionalCellActions(props), {
|
||||
initialProps,
|
||||
wrapper: ({ children }) => (
|
||||
<KibanaContextProvider services={discoverServiceMock}>{children}</KibanaContextProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
discoverServiceMock.profilesManager = createContextAwarenessMocks().profilesManagerMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockUuid = 0;
|
||||
});
|
||||
|
||||
it('should return metadata', async () => {
|
||||
const { result, unmount } = render();
|
||||
expect(result.current).toEqual({
|
||||
instanceId: '1',
|
||||
...initialProps,
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should register and unregister cell actions', async () => {
|
||||
await discoverServiceMock.profilesManager.resolveRootProfile({});
|
||||
const { rerender, result, unmount } = render();
|
||||
expect(result.current.instanceId).toEqual('1');
|
||||
expect(mockActions).toHaveLength(1);
|
||||
expect(mockTriggerActions[DISCOVER_CELL_ACTIONS_TRIGGER.id]).toEqual(['root-action-2']);
|
||||
await act(() => discoverServiceMock.profilesManager.resolveDataSourceProfile({}));
|
||||
rerender();
|
||||
expect(result.current.instanceId).toEqual('3');
|
||||
expect(mockActions).toHaveLength(2);
|
||||
expect(mockTriggerActions[DISCOVER_CELL_ACTIONS_TRIGGER.id]).toEqual([
|
||||
'root-action-4',
|
||||
'data-source-action-5',
|
||||
]);
|
||||
unmount();
|
||||
expect(mockActions).toHaveLength(0);
|
||||
expect(mockTriggerActions[DISCOVER_CELL_ACTIONS_TRIGGER.id]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCellAction', () => {
|
||||
const context: ActionExecutionContext<DiscoverCellActionExecutionContext> = {
|
||||
data: [
|
||||
{
|
||||
field: dataViewWithTimefieldMock.getFieldByName('message')?.toSpec()!,
|
||||
value: 'test message',
|
||||
},
|
||||
],
|
||||
metadata: undefined,
|
||||
nodeRef: React.createRef(),
|
||||
trigger: DISCOVER_CELL_ACTIONS_TRIGGER,
|
||||
};
|
||||
|
||||
const getCellAction = (isCompatible?: AdditionalCellAction['isCompatible']) => {
|
||||
const additional: AdditionalCellAction = {
|
||||
id: 'test',
|
||||
getIconType: jest.fn(() => 'plus'),
|
||||
getDisplayName: jest.fn(() => 'displayName'),
|
||||
execute: jest.fn(),
|
||||
isCompatible,
|
||||
};
|
||||
return { additional, action: createCellAction('test', additional, 0) };
|
||||
};
|
||||
|
||||
it('should create cell action', () => {
|
||||
const { action } = getCellAction();
|
||||
expect(action).toEqual({
|
||||
id: 'test-1',
|
||||
order: 0,
|
||||
type: DISCOVER_CELL_ACTION_TYPE,
|
||||
getIconType: expect.any(Function),
|
||||
getDisplayName: expect.any(Function),
|
||||
getDisplayNameTooltip: expect.any(Function),
|
||||
execute: expect.any(Function),
|
||||
isCompatible: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should get icon type', () => {
|
||||
const { additional, action } = getCellAction();
|
||||
expect(action.getIconType(context)).toEqual('plus');
|
||||
expect(additional.getIconType).toHaveBeenCalledWith(toCellActionContext(context));
|
||||
});
|
||||
|
||||
it('should get display name', () => {
|
||||
const { additional, action } = getCellAction();
|
||||
expect(action.getDisplayName(context)).toEqual('displayName');
|
||||
expect(action.getDisplayNameTooltip?.(context)).toEqual('displayName');
|
||||
expect(additional.getDisplayName).toHaveBeenCalledWith(toCellActionContext(context));
|
||||
});
|
||||
|
||||
it('should execute', async () => {
|
||||
const { additional, action } = getCellAction();
|
||||
await action.execute(context);
|
||||
expect(additional.execute).toHaveBeenCalledWith(toCellActionContext(context));
|
||||
});
|
||||
|
||||
it('should be compatible if isCompatible is undefined', async () => {
|
||||
const { action } = getCellAction();
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
metadata: { instanceId: 'test', dataView: dataViewWithTimefieldMock },
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should be compatible if isCompatible returns true', async () => {
|
||||
const { action } = getCellAction(() => true);
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
metadata: { instanceId: 'test', dataView: dataViewWithTimefieldMock },
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be compatible if isCompatible returns false', async () => {
|
||||
const { action } = getCellAction(() => false);
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
metadata: { instanceId: 'test', dataView: dataViewWithTimefieldMock },
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be compatible if instanceId is not equal', async () => {
|
||||
const { action } = getCellAction();
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
metadata: { instanceId: 'test2', dataView: dataViewWithTimefieldMock },
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be compatible if no data', async () => {
|
||||
const { action } = getCellAction();
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
data: [],
|
||||
metadata: { instanceId: 'test', dataView: dataViewWithTimefieldMock },
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("should not be compatible if field doesn't exist in data view", async () => {
|
||||
const { action } = getCellAction();
|
||||
expect(
|
||||
await action.isCompatible({
|
||||
...context,
|
||||
data: [
|
||||
{
|
||||
field: new DataViewField({
|
||||
name: 'test',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
metadata: { instanceId: 'test', dataView: dataViewWithTimefieldMock },
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { createCellActionFactory } from '@kbn/cell-actions/actions';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
DISCOVER_CELL_ACTIONS_TRIGGER,
|
||||
type AdditionalCellAction,
|
||||
type AdditionalCellActionContext,
|
||||
type DiscoverCellAction,
|
||||
type DiscoverCellActionExecutionContext,
|
||||
type DiscoverCellActionMetadata,
|
||||
} from '../types';
|
||||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
import { useProfileAccessor } from './use_profile_accessor';
|
||||
|
||||
export const DISCOVER_CELL_ACTION_TYPE = 'discover-cellAction-type';
|
||||
|
||||
export const useAdditionalCellActions = ({
|
||||
dataSource,
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
timeRange,
|
||||
}: Omit<AdditionalCellActionContext, 'field' | 'value'>) => {
|
||||
const { uiActions } = useDiscoverServices();
|
||||
const [instanceId, setInstanceId] = useState<string | undefined>();
|
||||
const getAdditionalCellActionsAccessor = useProfileAccessor('getAdditionalCellActions');
|
||||
const additionalCellActions = useMemo(
|
||||
() => getAdditionalCellActionsAccessor(() => [])(),
|
||||
[getAdditionalCellActionsAccessor]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const currentInstanceId = uuidv4();
|
||||
const actions = additionalCellActions.map((action, i) =>
|
||||
createCellAction(currentInstanceId, action, i)
|
||||
);
|
||||
|
||||
actions.forEach((action) => {
|
||||
uiActions.registerAction(action);
|
||||
uiActions.attachAction(DISCOVER_CELL_ACTIONS_TRIGGER.id, action.id);
|
||||
});
|
||||
|
||||
setInstanceId(currentInstanceId);
|
||||
|
||||
return () => {
|
||||
actions.forEach((action) => {
|
||||
uiActions.detachAction(DISCOVER_CELL_ACTIONS_TRIGGER.id, action.id);
|
||||
uiActions.unregisterAction(action.id);
|
||||
});
|
||||
|
||||
setInstanceId(undefined);
|
||||
};
|
||||
}, [additionalCellActions, uiActions]);
|
||||
|
||||
return useMemo<DiscoverCellActionMetadata>(
|
||||
() => ({ instanceId, dataSource, dataView, query, filters, timeRange }),
|
||||
[dataSource, dataView, filters, instanceId, query, timeRange]
|
||||
);
|
||||
};
|
||||
|
||||
export const createCellAction = (
|
||||
instanceId: string,
|
||||
action: AdditionalCellAction,
|
||||
order: number
|
||||
) => {
|
||||
const createFactory = createCellActionFactory<DiscoverCellAction>(() => ({
|
||||
type: DISCOVER_CELL_ACTION_TYPE,
|
||||
getIconType: (context) => action.getIconType(toCellActionContext(context)),
|
||||
getDisplayName: (context) => action.getDisplayName(toCellActionContext(context)),
|
||||
getDisplayNameTooltip: (context) => action.getDisplayName(toCellActionContext(context)),
|
||||
execute: async (context) => action.execute(toCellActionContext(context)),
|
||||
isCompatible: async ({ data, metadata }) => {
|
||||
if (metadata?.instanceId !== instanceId || data.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const field = data[0]?.field;
|
||||
|
||||
if (!field || !metadata.dataView?.getFieldByName(field.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return action.isCompatible?.({ field, ...metadata }) ?? true;
|
||||
},
|
||||
}));
|
||||
|
||||
const factory = createFactory();
|
||||
|
||||
return factory({ id: `${action.id}-${uuidv4()}`, order });
|
||||
};
|
||||
|
||||
export const toCellActionContext = ({
|
||||
data,
|
||||
metadata,
|
||||
}: DiscoverCellActionExecutionContext): AdditionalCellActionContext => ({
|
||||
...data[0],
|
||||
...metadata,
|
||||
});
|
|
@ -11,4 +11,4 @@ export * from './types';
|
|||
export * from './profiles';
|
||||
export { getMergedAccessor } from './composable_profile';
|
||||
export { ProfilesManager } from './profiles_manager';
|
||||
export { useProfileAccessor, useRootProfile } from './hooks';
|
||||
export { useProfileAccessor, useRootProfile, useAdditionalCellActions } from './hooks';
|
||||
|
|
|
@ -115,6 +115,27 @@ export const exampleDataSourceProfileProvider: DataSourceProfileProvider = {
|
|||
],
|
||||
rowHeight: 5,
|
||||
}),
|
||||
getAdditionalCellActions: (prev) => () =>
|
||||
[
|
||||
...prev(),
|
||||
{
|
||||
id: 'example-data-source-action',
|
||||
getDisplayName: () => 'Example data source action',
|
||||
getIconType: () => 'plus',
|
||||
execute: () => {
|
||||
alert('Example data source action executed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'another-example-data-source-action',
|
||||
getDisplayName: () => 'Another example data source action',
|
||||
getIconType: () => 'minus',
|
||||
execute: () => {
|
||||
alert('Another example data source action executed');
|
||||
},
|
||||
isCompatible: ({ field }) => field.name !== 'message',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: (params) => {
|
||||
let indexPattern: string | undefined;
|
||||
|
|
|
@ -11,6 +11,12 @@ import type { DataView } from '@kbn/data-views-plugin/common';
|
|||
import type { CustomCellRenderer, UnifiedDataTableProps } from '@kbn/unified-data-table';
|
||||
import type { DocViewsRegistry } from '@kbn/unified-doc-viewer';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils';
|
||||
import type { CellAction, CellActionExecutionContext, CellActionsData } from '@kbn/cell-actions';
|
||||
import type { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import type { OmitIndexSignature } from 'type-fest';
|
||||
import type { Trigger } from '@kbn/ui-actions-plugin/public';
|
||||
import type { DiscoverDataSource } from '../../common/data_sources';
|
||||
|
||||
export interface DocViewerExtension {
|
||||
title: string | undefined;
|
||||
|
@ -43,6 +49,36 @@ export interface RowControlsExtensionParams {
|
|||
dataView: DataView;
|
||||
}
|
||||
|
||||
export const DISCOVER_CELL_ACTIONS_TRIGGER: Trigger = { id: 'DISCOVER_CELL_ACTIONS_TRIGGER_ID' };
|
||||
|
||||
export interface DiscoverCellActionMetadata extends Record<string, unknown> {
|
||||
instanceId?: string;
|
||||
dataSource?: DiscoverDataSource;
|
||||
dataView?: DataView;
|
||||
query?: Query | AggregateQuery;
|
||||
filters?: Filter[];
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export interface DiscoverCellActionExecutionContext extends CellActionExecutionContext {
|
||||
metadata: DiscoverCellActionMetadata | undefined;
|
||||
}
|
||||
|
||||
export type DiscoverCellAction = CellAction<DiscoverCellActionExecutionContext>;
|
||||
|
||||
export type AdditionalCellActionContext = CellActionsData &
|
||||
Omit<OmitIndexSignature<DiscoverCellActionMetadata>, 'instanceId'>;
|
||||
|
||||
export interface AdditionalCellAction {
|
||||
id: string;
|
||||
getDisplayName: (context: AdditionalCellActionContext) => string;
|
||||
getIconType: (context: AdditionalCellActionContext) => EuiIconType;
|
||||
isCompatible?: (
|
||||
context: Omit<AdditionalCellActionContext, 'value'>
|
||||
) => boolean | Promise<boolean>;
|
||||
execute: (context: AdditionalCellActionContext) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
getDefaultAppState: (params: DefaultAppStateExtensionParams) => DefaultAppStateExtension;
|
||||
// Data grid
|
||||
|
@ -53,6 +89,7 @@ export interface Profile {
|
|||
getRowAdditionalLeadingControls: (
|
||||
params: RowControlsExtensionParams
|
||||
) => UnifiedDataTableProps['rowAdditionalLeadingControls'] | undefined;
|
||||
getAdditionalCellActions: () => AdditionalCellAction[];
|
||||
// Doc viewer
|
||||
getDocViewer: (params: DocViewerExtensionParams) => DocViewerExtension;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '@kbn/discover-utils';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
FetchContext,
|
||||
useBatchedOptionalPublishingSubjects,
|
||||
useBatchedPublishingSubjects,
|
||||
} from '@kbn/presentation-publishing';
|
||||
|
@ -28,6 +29,7 @@ import { DataGridDensity, DataLoadingState, useColumns } from '@kbn/unified-data
|
|||
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
|
||||
|
||||
import { DiscoverGridSettings } from '@kbn/saved-search-plugin/common';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { DiscoverDocTableEmbeddable } from '../../components/doc_table/create_doc_table_embeddable';
|
||||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
import { getSortForEmbeddable } from '../../utils';
|
||||
|
@ -38,9 +40,15 @@ import type { SearchEmbeddableApi, SearchEmbeddableStateManager } from '../types
|
|||
import { DiscoverGridEmbeddable } from './saved_search_grid';
|
||||
import { getSearchEmbeddableDefaults } from '../get_search_embeddable_defaults';
|
||||
import { onResizeGridColumn } from '../../utils/on_resize_grid_column';
|
||||
import { DISCOVER_CELL_ACTIONS_TRIGGER, useAdditionalCellActions } from '../../context_awareness';
|
||||
import { getTimeRangeFromFetchContext } from '../utils/update_search_source';
|
||||
import { createDataSource } from '../../../common/data_sources';
|
||||
|
||||
interface SavedSearchEmbeddableComponentProps {
|
||||
api: SearchEmbeddableApi & { fetchWarnings$: BehaviorSubject<SearchResponseIncompleteWarning[]> };
|
||||
api: SearchEmbeddableApi & {
|
||||
fetchWarnings$: BehaviorSubject<SearchResponseIncompleteWarning[]>;
|
||||
fetchContext$: BehaviorSubject<FetchContext | undefined>;
|
||||
};
|
||||
dataView: DataView;
|
||||
onAddFilter?: DocViewFilterFn;
|
||||
stateManager: SearchEmbeddableStateManager;
|
||||
|
@ -61,6 +69,9 @@ export function SearchEmbeddableGridComponent({
|
|||
savedSearch,
|
||||
savedSearchId,
|
||||
interceptedWarnings,
|
||||
query,
|
||||
filters,
|
||||
fetchContext,
|
||||
rows,
|
||||
totalHitCount,
|
||||
columnsMeta,
|
||||
|
@ -70,6 +81,9 @@ export function SearchEmbeddableGridComponent({
|
|||
api.savedSearch$,
|
||||
api.savedObjectId,
|
||||
api.fetchWarnings$,
|
||||
api.query$,
|
||||
api.filters$,
|
||||
api.fetchContext$,
|
||||
stateManager.rows,
|
||||
stateManager.totalHitCount,
|
||||
stateManager.columnsMeta,
|
||||
|
@ -123,6 +137,25 @@ export function SearchEmbeddableGridComponent({
|
|||
settings: grid,
|
||||
});
|
||||
|
||||
const dataSource = useMemo(() => createDataSource({ dataView, query }), [dataView, query]);
|
||||
const timeRange = useMemo(
|
||||
() => (fetchContext ? getTimeRangeFromFetchContext(fetchContext) : undefined),
|
||||
[fetchContext]
|
||||
);
|
||||
|
||||
const cellActionsMetadata = useAdditionalCellActions({
|
||||
dataSource,
|
||||
dataView,
|
||||
query,
|
||||
filters,
|
||||
timeRange,
|
||||
});
|
||||
|
||||
// Security Solution overrides our cell actions -- this is a temporary workaroud to keep
|
||||
// things working as they do currently until we can migrate their actions to One Discover
|
||||
const isInSecuritySolution =
|
||||
useObservable(discoverServices.application.currentAppId$) === 'securitySolutionUI';
|
||||
|
||||
const onStateEditedProps = useMemo(
|
||||
() => ({
|
||||
onAddColumn,
|
||||
|
@ -210,7 +243,13 @@ export function SearchEmbeddableGridComponent({
|
|||
{...onStateEditedProps}
|
||||
settings={savedSearch.grid}
|
||||
ariaLabelledBy={'documentsAriaLabel'}
|
||||
cellActionsTriggerId={SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID}
|
||||
cellActionsTriggerId={
|
||||
isInSecuritySolution
|
||||
? SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID
|
||||
: DISCOVER_CELL_ACTIONS_TRIGGER.id
|
||||
}
|
||||
cellActionsMetadata={isInSecuritySolution ? undefined : cellActionsMetadata}
|
||||
cellActionsHandling={isInSecuritySolution ? 'replace' : 'append'}
|
||||
columnsMeta={columnsMeta}
|
||||
configHeaderRowHeight={defaults.headerRowHeight}
|
||||
configRowHeight={defaults.rowHeight}
|
||||
|
|
|
@ -297,7 +297,7 @@ export const getSearchEmbeddableFactory = ({
|
|||
}
|
||||
>
|
||||
<SearchEmbeddableGridComponent
|
||||
api={{ ...api, fetchWarnings$ }}
|
||||
api={{ ...api, fetchWarnings$, fetchContext$ }}
|
||||
dataView={dataView!}
|
||||
onAddFilter={isEsqlMode(savedSearch) ? undefined : onAddFilter}
|
||||
stateManager={searchEmbeddable.stateManager}
|
||||
|
|
|
@ -36,13 +36,13 @@ import { SearchResponseWarning } from '@kbn/search-response-warnings';
|
|||
import { SearchResponseIncompleteWarning } from '@kbn/search-response-warnings/src/types';
|
||||
import { getTextBasedColumnsMeta } from '@kbn/unified-data-table';
|
||||
|
||||
import { createDataViewDataSource, createEsqlDataSource } from '../../common/data_sources';
|
||||
import { fetchEsql } from '../application/main/data_fetching/fetch_esql';
|
||||
import { DiscoverServices } from '../build_services';
|
||||
import { getAllowedSampleSize } from '../utils/get_allowed_sample_size';
|
||||
import { getAppTarget } from './initialize_edit_api';
|
||||
import { PublishesSavedSearch, SearchEmbeddableStateManager } from './types';
|
||||
import { getTimeRangeFromFetchContext, updateSearchSource } from './utils/update_search_source';
|
||||
import { createDataSource } from '../../common/data_sources';
|
||||
|
||||
type SavedSearchPartialFetchApi = PublishesSavedSearch &
|
||||
PublishesSavedObjectId &
|
||||
|
@ -138,11 +138,7 @@ export function initializeFetch({
|
|||
abortController = currentAbortController;
|
||||
|
||||
await discoverServices.profilesManager.resolveDataSourceProfile({
|
||||
dataSource: isOfAggregateQueryType(searchSourceQuery)
|
||||
? createEsqlDataSource()
|
||||
: dataView.id
|
||||
? createDataViewDataSource({ dataViewId: dataView.id })
|
||||
: undefined,
|
||||
dataSource: createDataSource({ dataView, query: searchSourceQuery }),
|
||||
dataView,
|
||||
query: searchSourceQuery,
|
||||
});
|
||||
|
|
|
@ -53,14 +53,13 @@ import {
|
|||
import { getESQLSearchProvider } from './global_search/search_provider';
|
||||
import { HistoryService } from './history_service';
|
||||
import type { ConfigSchema, ExperimentalFeatures } from '../server/config';
|
||||
import {
|
||||
DataSourceProfileService,
|
||||
DocumentProfileService,
|
||||
ProfilesManager,
|
||||
RootProfileService,
|
||||
} from './context_awareness';
|
||||
import { DiscoverSetup, DiscoverSetupPlugins, DiscoverStart, DiscoverStartPlugins } from './types';
|
||||
import { deserializeState } from './embeddable/utils/serialization_utils';
|
||||
import { DISCOVER_CELL_ACTIONS_TRIGGER } from './context_awareness/types';
|
||||
import { RootProfileService } from './context_awareness/profiles/root_profile';
|
||||
import { DataSourceProfileService } from './context_awareness/profiles/data_source_profile';
|
||||
import { DocumentProfileService } from './context_awareness/profiles/document_profile';
|
||||
import { ProfilesManager } from './context_awareness/profiles_manager';
|
||||
|
||||
/**
|
||||
* Contains Discover, one of the oldest parts of Kibana
|
||||
|
@ -271,6 +270,7 @@ export class DiscoverPlugin
|
|||
|
||||
plugins.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction);
|
||||
plugins.uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER);
|
||||
plugins.uiActions.registerTrigger(DISCOVER_CELL_ACTIONS_TRIGGER);
|
||||
injectTruncateStyles(core.uiSettings.get(TRUNCATE_MAX_HEIGHT));
|
||||
|
||||
const isEsqlEnabled = core.uiSettings.get(ENABLE_ESQL);
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 kbnRison from '@kbn/rison';
|
||||
import expect from '@kbn/expect';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'header', 'unifiedFieldList']);
|
||||
const dataViews = getService('dataViews');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const browser = getService('browser');
|
||||
|
||||
describe('extension getAdditionalCellActions', () => {
|
||||
describe('ES|QL mode', () => {
|
||||
it('should render additional cell actions for logs data source', async () => {
|
||||
const state = kbnRison.encode({
|
||||
dataSource: { type: 'esql' },
|
||||
query: { esql: 'from my-example-logs | sort @timestamp desc' },
|
||||
});
|
||||
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('example-data-source-action');
|
||||
let alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('another-example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Another example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not render incompatible cell action for message column', async () => {
|
||||
const state = kbnRison.encode({
|
||||
dataSource: { type: 'esql' },
|
||||
query: { esql: 'from my-example-logs | sort @timestamp desc' },
|
||||
});
|
||||
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 2);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
|
||||
it('should not render cell actions for incompatible data source', async () => {
|
||||
const state = kbnRison.encode({
|
||||
dataSource: { type: 'esql' },
|
||||
query: { esql: 'from my-example-metrics | sort @timestamp desc' },
|
||||
});
|
||||
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data view mode', () => {
|
||||
it('should render additional cell actions for logs data source', async () => {
|
||||
await PageObjects.common.navigateToActualUrl('discover', undefined, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await dataViews.switchTo('my-example-logs');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('example-data-source-action');
|
||||
let alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('another-example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Another example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
// check Surrounding docs page
|
||||
await dataGrid.clickRowToggle();
|
||||
const [, surroundingActionEl] = await dataGrid.getRowActions();
|
||||
await surroundingActionEl.click();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await browser.refresh();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('another-example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Another example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not render incompatible cell action for message column', async () => {
|
||||
await PageObjects.common.navigateToActualUrl('discover', undefined, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await dataViews.switchTo('my-example-logs');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 2);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
|
||||
it('should not render cell actions for incompatible data source', async () => {
|
||||
await PageObjects.common.navigateToActualUrl('discover', undefined, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await dataViews.switchTo('my-example-metrics');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -43,5 +43,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
|
|||
loadTestFile(require.resolve('./extensions/_get_doc_viewer'));
|
||||
loadTestFile(require.resolve('./extensions/_get_cell_renderers'));
|
||||
loadTestFile(require.resolve('./extensions/_get_default_app_state'));
|
||||
loadTestFile(require.resolve('./extensions/_get_additional_cell_actions'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -173,6 +173,23 @@ export class DataGridService extends FtrService {
|
|||
await this.clickCellExpandButton(rowIndex, controlsCount + columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a cell action button within the expanded cell popover
|
||||
* @param cellActionId The ID of the registered cell action
|
||||
*/
|
||||
public async clickCellExpandPopoverAction(cellActionId: string) {
|
||||
await this.testSubjects.click(`*dataGridColumnCellAction-${cellActionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cell action button exists within the expanded cell popover
|
||||
* @param cellActionId The ID of the registered cell action
|
||||
* @returns If the cell action button exists
|
||||
*/
|
||||
public async cellExpandPopoverActionExists(cellActionId: string) {
|
||||
return await this.testSubjects.exists(`*dataGridColumnCellAction-${cellActionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks grid cell 'filter for' action button
|
||||
* @param rowIndex data row index starting from 0 (0 means 1st row)
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import kbnRison from '@kbn/rison';
|
||||
import expect from '@kbn/expect';
|
||||
import type { FtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects([
|
||||
'common',
|
||||
'discover',
|
||||
'header',
|
||||
'unifiedFieldList',
|
||||
'svlCommonPage',
|
||||
]);
|
||||
const dataViews = getService('dataViews');
|
||||
const dataGrid = getService('dataGrid');
|
||||
const browser = getService('browser');
|
||||
|
||||
describe('extension getAdditionalCellActions', () => {
|
||||
before(async () => {
|
||||
await PageObjects.svlCommonPage.loginAsAdmin();
|
||||
});
|
||||
|
||||
describe('ES|QL mode', () => {
|
||||
it('should render additional cell actions for logs data source', async () => {
|
||||
const state = kbnRison.encode({
|
||||
dataSource: { type: 'esql' },
|
||||
query: { esql: 'from my-example-logs | sort @timestamp desc' },
|
||||
});
|
||||
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('example-data-source-action');
|
||||
let alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('another-example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Another example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not render incompatible cell action for message column', async () => {
|
||||
const state = kbnRison.encode({
|
||||
dataSource: { type: 'esql' },
|
||||
query: { esql: 'from my-example-logs | sort @timestamp desc' },
|
||||
});
|
||||
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 2);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
|
||||
it('should not render cell actions for incompatible data source', async () => {
|
||||
const state = kbnRison.encode({
|
||||
dataSource: { type: 'esql' },
|
||||
query: { esql: 'from my-example-metrics | sort @timestamp desc' },
|
||||
});
|
||||
await PageObjects.common.navigateToActualUrl('discover', `?_a=${state}`, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data view mode', () => {
|
||||
it('should render additional cell actions for logs data source', async () => {
|
||||
await PageObjects.common.navigateToActualUrl('discover', undefined, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await dataViews.switchTo('my-example-logs');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('example-data-source-action');
|
||||
let alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('another-example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Another example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
// check Surrounding docs page
|
||||
await dataGrid.clickRowToggle();
|
||||
const [, surroundingActionEl] = await dataGrid.getRowActions();
|
||||
await surroundingActionEl.click();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await browser.refresh();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
await dataGrid.clickCellExpandPopoverAction('another-example-data-source-action');
|
||||
alert = await browser.getAlert();
|
||||
try {
|
||||
expect(await alert?.getText()).to.be('Another example data source action executed');
|
||||
} finally {
|
||||
await alert?.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not render incompatible cell action for message column', async () => {
|
||||
await PageObjects.common.navigateToActualUrl('discover', undefined, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await dataViews.switchTo('my-example-logs');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 2);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
|
||||
it('should not render cell actions for incompatible data source', async () => {
|
||||
await PageObjects.common.navigateToActualUrl('discover', undefined, {
|
||||
ensureCurrentUrl: false,
|
||||
});
|
||||
await dataViews.switchTo('my-example-metrics');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
await dataGrid.clickCellExpandButtonExcludingControlColumns(0, 0);
|
||||
expect(await dataGrid.cellExpandPopoverActionExists('example-data-source-action')).to.be(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
await dataGrid.cellExpandPopoverActionExists('another-example-data-source-action')
|
||||
).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -42,5 +42,6 @@ export default function ({ getService, getPageObjects, loadTestFile }: FtrProvid
|
|||
loadTestFile(require.resolve('./extensions/_get_doc_viewer'));
|
||||
loadTestFile(require.resolve('./extensions/_get_cell_renderers'));
|
||||
loadTestFile(require.resolve('./extensions/_get_default_app_state'));
|
||||
loadTestFile(require.resolve('./extensions/_get_additional_cell_actions'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue