mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] Use CellActions registry in Discover data grid (#157201)
## Summary closes: https://github.com/elastic/kibana/issues/157191 Enables Discover DataGrid to use registered cell actions instead of the default static actions. ### New `cellActionsTriggerId` prop This PR introduces a new `cellActionsTriggerId` _optional_ prop in the DataGrid component:98c210f9ec/src/plugins/discover/public/components/discover_grid/discover_grid.tsx (L198-L201)
When this prop is defined, the component queries the trigger's registry to retrieve the cellActions attached to it, using the CellActions package' `useDataGridColumnsCellActions` hook. This hook returns the cellActions array ready to be passed for each column to the EuiDataGrid component. When (non-empty) actions are found in the registry, they are used, replacing all of the default static Discover actions. Otherwise, the default cell actions are used. This new prop also allows other instances of the Discover DataGrid to be configured with custom cell actions, which will probably be needed by Security Timeline integration with Discover. ### New `SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER` Trigger Along with the new `cellActionsTriggerId` prop the plugin also registers a new trigger for "saved search" embeddable:055750c8dd/src/plugins/discover/public/plugin.tsx (L387)
And it gets passed to the DataGrid component on the Embeddable creation:055750c8dd/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx (L403)
Having this new trigger available allows solutions to attach custom actions to it, in order to be displayed in the saved search embeddables. Each action will be able to implement its `isCompatible` check to determine if they are going to be displayed in the embedded saved search DataGrid field, or not. If no compatible actions are found, DataGrid will render the default static actions. ℹ️ In this implementation, the actions registered to this new "embeddable trigger" need to check if they are being rendered inside Security using the `isCompatible` function, to prevent them from being displayed in other solutions, resulting in a non-optimal architecture. This approach was needed since there's no plausible way to pass the `cellActionsTriggerId` property from the Dashboard Renderer used in Security, all the way down to the specific Discover "saved search" embeddable. However, the Dashboards team is planning to enable us to pass options to nested embeddables using a registry (https://github.com/elastic/kibana/issues/148933). When this new tool is available we will be able to delegate the trigger registering to Security and configure the "saved search" embeddables to use it. Therefore, the trigger will only be used by Security, so we won't have to worry about Security actions being rendered outside Security. ## Videos before:de92cd74
-6125-4766-8e9d-7e0985932618 after:f9bd597a
-860e-4572-aa9d-9f1c72c11a4b --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Pablo Neves Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
parent
5e72c03a9a
commit
f4159c4583
52 changed files with 1667 additions and 730 deletions
|
@ -6,6 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
|
||||
export const FILTER_CELL_ACTION_TYPE = 'cellAction-filter';
|
||||
export const COPY_CELL_ACTION_TYPE = 'cellAction-copy';
|
||||
|
||||
|
@ -14,3 +16,10 @@ export enum CellActionsMode {
|
|||
HOVER_RIGHT = 'hover-right',
|
||||
INLINE = 'inline',
|
||||
}
|
||||
|
||||
export const SUPPORTED_KBN_TYPES = [
|
||||
KBN_FIELD_TYPES.DATE,
|
||||
KBN_FIELD_TYPES.IP,
|
||||
KBN_FIELD_TYPES.STRING,
|
||||
KBN_FIELD_TYPES.NUMBER, // Currently supported by casting https://github.com/elastic/kibana/issues/159298
|
||||
];
|
||||
|
|
|
@ -7,26 +7,30 @@
|
|||
*/
|
||||
|
||||
import { orderBy } from 'lodash/fp';
|
||||
import React, { createContext, FC, useCallback, useContext } from 'react';
|
||||
import React, { createContext, FC, useContext, useMemo } from 'react';
|
||||
import type { CellAction, CellActionsProviderProps, GetActions } from '../types';
|
||||
|
||||
const CellActionsContext = createContext<{ getActions: GetActions } | null>(null);
|
||||
interface CellActionsContextValue {
|
||||
getActions: GetActions;
|
||||
}
|
||||
const CellActionsContext = createContext<CellActionsContextValue | null>(null);
|
||||
|
||||
export const CellActionsProvider: FC<CellActionsProviderProps> = ({
|
||||
children,
|
||||
getTriggerCompatibleActions,
|
||||
}) => {
|
||||
const getActions = useCallback<GetActions>(
|
||||
(context) =>
|
||||
getTriggerCompatibleActions(context.trigger.id, context).then((actions) =>
|
||||
orderBy(['order', 'id'], ['asc', 'asc'], actions)
|
||||
) as Promise<CellAction[]>,
|
||||
const value = useMemo<CellActionsContextValue>(
|
||||
() => ({
|
||||
getActions: (context) =>
|
||||
getTriggerCompatibleActions(context.trigger.id, context).then((actions) =>
|
||||
orderBy(['order', 'id'], ['asc', 'asc'], actions)
|
||||
) as Promise<CellAction[]>,
|
||||
}),
|
||||
[getTriggerCompatibleActions]
|
||||
);
|
||||
|
||||
return (
|
||||
<CellActionsContext.Provider value={{ getActions }}>{children}</CellActionsContext.Provider>
|
||||
);
|
||||
// make sure that provider's value does not change
|
||||
return <CellActionsContext.Provider value={value}>{children}</CellActionsContext.Provider>;
|
||||
};
|
||||
|
||||
export const useCellActionsContext = () => {
|
||||
|
|
|
@ -31,30 +31,21 @@ const mockGetActions = jest.fn(async () => actions);
|
|||
jest.mock('../context/cell_actions_context', () => ({
|
||||
useCellActionsContext: () => ({ getActions: mockGetActions }),
|
||||
}));
|
||||
const values1 = ['0.0', '0.1', '0.2', '0.3'];
|
||||
const field1 = {
|
||||
name: 'column1',
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
};
|
||||
|
||||
const values2 = ['1.0', '1.1', '1.2', '1.3'];
|
||||
const field2 = {
|
||||
name: 'column2',
|
||||
|
||||
type: 'string',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
const fieldValues: Record<string, string[]> = {
|
||||
column1: ['0.0', '0.1', '0.2', '0.3'],
|
||||
column2: ['1.0', '1.1', '1.2', '1.3'],
|
||||
};
|
||||
const mockGetCellValue = jest.fn(
|
||||
(field: string, rowIndex: number) => fieldValues[field]?.[rowIndex % fieldValues[field].length]
|
||||
);
|
||||
const field1 = { name: 'column1', type: 'text', searchable: true, aggregatable: true };
|
||||
const field2 = { name: 'column2', type: 'keyword', searchable: true, aggregatable: true };
|
||||
const columns = [{ id: field1.name }, { id: field2.name }];
|
||||
|
||||
const mockCloseCellPopover = jest.fn();
|
||||
const useDataGridColumnsCellActionsProps: UseDataGridColumnsCellActionsProps = {
|
||||
data: [
|
||||
{ field: field1, values: values1 },
|
||||
{ field: field2, values: values2 },
|
||||
],
|
||||
fields: [field1, field2],
|
||||
getCellValue: mockGetCellValue,
|
||||
triggerId: 'testTriggerId',
|
||||
metadata: { some: 'value' },
|
||||
dataGridRef: {
|
||||
|
@ -101,13 +92,31 @@ describe('useDataGridColumnsCellActions', () => {
|
|||
const { result } = renderHook(useDataGridColumnsCellActions, {
|
||||
initialProps: useDataGridColumnsCellActionsProps,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const cellAction = renderCellAction(result.current[0][0]);
|
||||
expect(cellAction.getByTestId('dataGridColumnCellAction-loading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call getCellValue with the proper params', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
|
||||
initialProps: useDataGridColumnsCellActionsProps,
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
renderCellAction(result.current[0][0], { rowIndex: 0 });
|
||||
renderCellAction(result.current[0][1], { rowIndex: 1 });
|
||||
renderCellAction(result.current[1][0], { rowIndex: 0 });
|
||||
renderCellAction(result.current[1][1], { rowIndex: 1 });
|
||||
|
||||
expect(mockGetCellValue).toHaveBeenCalledTimes(4);
|
||||
expect(mockGetCellValue).toHaveBeenCalledWith(field1.name, 0);
|
||||
expect(mockGetCellValue).toHaveBeenCalledWith(field1.name, 1);
|
||||
expect(mockGetCellValue).toHaveBeenCalledWith(field2.name, 0);
|
||||
expect(mockGetCellValue).toHaveBeenCalledWith(field2.name, 1);
|
||||
});
|
||||
|
||||
it('should render the cell actions', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
|
||||
initialProps: useDataGridColumnsCellActionsProps,
|
||||
|
@ -156,7 +165,7 @@ describe('useDataGridColumnsCellActions', () => {
|
|||
expect.objectContaining({
|
||||
data: [
|
||||
{
|
||||
value: values1[1],
|
||||
value: fieldValues[field1.name][1],
|
||||
field: {
|
||||
name: field1.name,
|
||||
type: field1.type,
|
||||
|
@ -179,7 +188,7 @@ describe('useDataGridColumnsCellActions', () => {
|
|||
expect.objectContaining({
|
||||
data: [
|
||||
{
|
||||
value: values2[2],
|
||||
value: fieldValues[field2.name][2],
|
||||
field: {
|
||||
name: field2.name,
|
||||
type: field2.type,
|
||||
|
@ -204,12 +213,14 @@ describe('useDataGridColumnsCellActions', () => {
|
|||
|
||||
cellAction.getByTestId(`dataGridColumnCellAction-${action1.id}`).click();
|
||||
|
||||
expect(mockGetCellValue).toHaveBeenCalledWith(field1.name, 25);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(action1.execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
{
|
||||
value: values1[1],
|
||||
value: fieldValues[field1.name][1],
|
||||
field: {
|
||||
name: field1.name,
|
||||
type: field1.type,
|
||||
|
@ -242,7 +253,7 @@ describe('useDataGridColumnsCellActions', () => {
|
|||
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
|
||||
initialProps: {
|
||||
...useDataGridColumnsCellActionsProps,
|
||||
data: [],
|
||||
fields: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -256,7 +267,7 @@ describe('useDataGridColumnsCellActions', () => {
|
|||
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
|
||||
initialProps: {
|
||||
...useDataGridColumnsCellActionsProps,
|
||||
data: undefined,
|
||||
fields: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -12,47 +12,66 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
type EuiDataGridColumnCellAction,
|
||||
} from '@elastic/eui';
|
||||
import { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import type {
|
||||
CellAction,
|
||||
CellActionCompatibilityContext,
|
||||
CellActionExecutionContext,
|
||||
CellActionsData,
|
||||
CellActionsProps,
|
||||
CellActionFieldValue,
|
||||
} from '../types';
|
||||
import { useBulkLoadActions } from './use_load_actions';
|
||||
|
||||
interface BulkData extends Omit<CellActionsData, 'value'> {
|
||||
/**
|
||||
* Array containing all the values of the field in the visible page, indexed by rowIndex
|
||||
*/
|
||||
values: Array<string | string[] | null | undefined>;
|
||||
}
|
||||
|
||||
export interface UseDataGridColumnsCellActionsProps
|
||||
extends Pick<CellActionsProps, 'triggerId' | 'metadata' | 'disabledActionTypes'> {
|
||||
data?: BulkData[];
|
||||
extends Pick<CellActionsProps, 'metadata' | 'disabledActionTypes'> {
|
||||
/**
|
||||
* Optional trigger ID to used to retrieve the cell actions.
|
||||
* returns empty array if not provided
|
||||
*/
|
||||
triggerId?: string;
|
||||
/**
|
||||
* fields array, used to determine which actions to load.
|
||||
* returns empty array if not provided
|
||||
*/
|
||||
fields?: FieldSpec[];
|
||||
/**
|
||||
* Function to get the cell value for a given field name and row index.
|
||||
* the `rowIndex` parameter is absolute, not relative to the current page
|
||||
*/
|
||||
getCellValue: (fieldName: string, rowIndex: number) => CellActionFieldValue;
|
||||
/**
|
||||
* ref to the EuiDataGrid instance
|
||||
*/
|
||||
dataGridRef: MutableRefObject<EuiDataGridRefProps | null>;
|
||||
}
|
||||
export type UseDataGridColumnsCellActions<
|
||||
P extends UseDataGridColumnsCellActionsProps = UseDataGridColumnsCellActionsProps
|
||||
> = (props: P) => EuiDataGridColumnCellAction[][];
|
||||
|
||||
// static actions array references to prevent React updates
|
||||
const loadingColumnActions: EuiDataGridColumnCellAction[] = [
|
||||
() => <EuiLoadingSpinner size="s" data-test-subj="dataGridColumnCellAction-loading" />,
|
||||
];
|
||||
const emptyActions: EuiDataGridColumnCellAction[][] = [];
|
||||
|
||||
export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({
|
||||
data,
|
||||
fields,
|
||||
getCellValue,
|
||||
triggerId,
|
||||
metadata,
|
||||
dataGridRef,
|
||||
disabledActionTypes = [],
|
||||
}) => {
|
||||
const bulkContexts: CellActionCompatibilityContext[] = useMemo(
|
||||
() =>
|
||||
data?.map(({ field }) => ({
|
||||
data: [{ field }], // we are getting the actions for the whole column field, so the compatibility check will be done without the value
|
||||
trigger: { id: triggerId },
|
||||
metadata,
|
||||
})) ?? [],
|
||||
[triggerId, metadata, data]
|
||||
);
|
||||
const bulkContexts: CellActionCompatibilityContext[] | undefined = useMemo(() => {
|
||||
if (!triggerId || !fields?.length) {
|
||||
return undefined;
|
||||
}
|
||||
return fields.map((field) => ({
|
||||
data: [{ field }],
|
||||
trigger: { id: triggerId },
|
||||
metadata,
|
||||
}));
|
||||
}, [fields, triggerId, metadata]);
|
||||
|
||||
const { loading, value: columnsActions } = useBulkLoadActions(bulkContexts, {
|
||||
disabledActionTypes,
|
||||
|
@ -60,46 +79,45 @@ export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({
|
|||
|
||||
const columnsCellActions = useMemo<EuiDataGridColumnCellAction[][]>(() => {
|
||||
if (loading) {
|
||||
return (
|
||||
data?.map(() => [
|
||||
() => <EuiLoadingSpinner size="s" data-test-subj="dataGridColumnCellAction-loading" />,
|
||||
]) ?? []
|
||||
);
|
||||
return fields?.length ? fields.map(() => loadingColumnActions) : emptyActions;
|
||||
}
|
||||
if (!columnsActions || !data || data.length === 0) {
|
||||
return [];
|
||||
if (!triggerId || !columnsActions?.length || !fields?.length) {
|
||||
return emptyActions;
|
||||
}
|
||||
|
||||
// Check for a temporary inconsistency because `useBulkLoadActions` takes one render loop before setting `loading` to true.
|
||||
// It will eventually update to a consistent state
|
||||
if (columnsActions.length !== data.length) {
|
||||
return [];
|
||||
if (columnsActions.length !== fields.length) {
|
||||
return emptyActions;
|
||||
}
|
||||
|
||||
return columnsActions.map((actions, columnIndex) =>
|
||||
actions.map((action) =>
|
||||
createColumnCellAction({
|
||||
action,
|
||||
field: fields[columnIndex],
|
||||
getCellValue,
|
||||
metadata,
|
||||
triggerId,
|
||||
data: data[columnIndex],
|
||||
dataGridRef,
|
||||
})
|
||||
)
|
||||
);
|
||||
}, [loading, columnsActions, data, metadata, triggerId, dataGridRef]);
|
||||
}, [columnsActions, fields, getCellValue, loading, metadata, triggerId, dataGridRef]);
|
||||
|
||||
return columnsCellActions;
|
||||
};
|
||||
|
||||
interface CreateColumnCellActionParams
|
||||
extends Pick<UseDataGridColumnsCellActionsProps, 'triggerId' | 'metadata' | 'dataGridRef'> {
|
||||
data: BulkData;
|
||||
extends Pick<UseDataGridColumnsCellActionsProps, 'getCellValue' | 'metadata' | 'dataGridRef'> {
|
||||
field: FieldSpec;
|
||||
triggerId: string;
|
||||
action: CellAction;
|
||||
}
|
||||
const createColumnCellAction = ({
|
||||
data: { field, values },
|
||||
action,
|
||||
field,
|
||||
getCellValue,
|
||||
metadata,
|
||||
triggerId,
|
||||
dataGridRef,
|
||||
|
@ -109,8 +127,8 @@ const createColumnCellAction = ({
|
|||
const buttonRef = useRef<HTMLAnchorElement | null>(null);
|
||||
|
||||
const actionContext: CellActionExecutionContext = useMemo(() => {
|
||||
// rowIndex refers to all pages, we need to use the row index relative to the page to get the value
|
||||
const value = values[rowIndex % values.length];
|
||||
const { name } = field;
|
||||
const value = getCellValue(name, rowIndex);
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
|
|
|
@ -199,5 +199,24 @@ describe('loadActions hooks', () => {
|
|||
expect(result.current.value).toHaveLength(0);
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should return the same array after re-render when contexts is undefined', async () => {
|
||||
const { result, rerender, waitFor } = renderHook(useBulkLoadActions, {
|
||||
initialProps: undefined,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.value).toEqual([]));
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(mockGetActions).not.toHaveBeenCalled();
|
||||
|
||||
const initialResultValue = result.current.value;
|
||||
|
||||
rerender(undefined);
|
||||
|
||||
await waitFor(() => expect(result.current.value).toEqual([]));
|
||||
expect(result.current.value).toBe(initialResultValue);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(mockGetActions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -53,18 +53,18 @@ interface LoadActionsOptions {
|
|||
* Groups getActions calls for an array of contexts in one async bulk operation
|
||||
*/
|
||||
export const useBulkLoadActions = (
|
||||
contexts: CellActionCompatibilityContext[],
|
||||
contexts: CellActionCompatibilityContext[] | undefined,
|
||||
options: LoadActionsOptions = {}
|
||||
): AsyncActions<CellAction[][]> => {
|
||||
const { getActions } = useCellActionsContext();
|
||||
const { error, ...actionsState } = useAsync(
|
||||
() =>
|
||||
Promise.all(
|
||||
contexts.map((context) =>
|
||||
contexts?.map((context) =>
|
||||
getActions(context).then(
|
||||
(actions) => filteredActions(actions, options.disabledActionTypes) ?? []
|
||||
)
|
||||
)
|
||||
) ?? []
|
||||
),
|
||||
[contexts]
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
// Types and enums
|
||||
export type {
|
||||
CellAction,
|
||||
CellActionFieldValue,
|
||||
CellActionsProps,
|
||||
CellActionExecutionContext,
|
||||
CellActionCompatibilityContext,
|
||||
|
|
36
packages/kbn-cell-actions/src/utils.test.ts
Normal file
36
packages/kbn-cell-actions/src/utils.test.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { isTypeSupportedByCellActions } from './utils';
|
||||
|
||||
describe('isTypeSupportedByCellActions', () => {
|
||||
it('returns true if the type is number', () => {
|
||||
expect(isTypeSupportedByCellActions(KBN_FIELD_TYPES.NUMBER)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if the type is string', () => {
|
||||
expect(isTypeSupportedByCellActions(KBN_FIELD_TYPES.STRING)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if the type is ip', () => {
|
||||
expect(isTypeSupportedByCellActions(KBN_FIELD_TYPES.IP)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true if the type is date', () => {
|
||||
expect(isTypeSupportedByCellActions(KBN_FIELD_TYPES.DATE)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the type is boolean', () => {
|
||||
expect(isTypeSupportedByCellActions(KBN_FIELD_TYPES.BOOLEAN)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if the type is unknown', () => {
|
||||
expect(isTypeSupportedByCellActions(KBN_FIELD_TYPES.BOOLEAN)).toBe(false);
|
||||
});
|
||||
});
|
13
packages/kbn-cell-actions/src/utils.ts
Normal file
13
packages/kbn-cell-actions/src/utils.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { KBN_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { SUPPORTED_KBN_TYPES } from './constants';
|
||||
|
||||
export const isTypeSupportedByCellActions = (kbnFieldType: KBN_FIELD_TYPES) =>
|
||||
SUPPORTED_KBN_TYPES.includes(kbnFieldType);
|
|
@ -19,6 +19,7 @@
|
|||
"@kbn/data-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/field-types",
|
||||
"@kbn/data-views-plugin",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
||||
const fields = [
|
||||
export const fields = [
|
||||
{
|
||||
name: '_source',
|
||||
type: '_source',
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Observable, of } from 'rxjs';
|
|||
import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
|
||||
import { DiscoverServices } from '../build_services';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
|
||||
import { savedSearchPluginMock } from '@kbn/saved-search-plugin/public/mocks';
|
||||
import { chromeServiceMock, coreMock, docLinksServiceMock } from '@kbn/core/public/mocks';
|
||||
|
@ -120,6 +121,7 @@ export function createDiscoverServicesMock(): DiscoverServices {
|
|||
inspector: {
|
||||
open: jest.fn(),
|
||||
},
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
uiSettings: {
|
||||
get: jest.fn((key: string) => {
|
||||
if (key === 'fields:popularLimit') {
|
||||
|
|
|
@ -15,14 +15,15 @@ import { SortDirection } from '@kbn/data-plugin/public';
|
|||
import { ContextAppContent, ContextAppContentProps } from './context_app_content';
|
||||
import { LoadingStatus } from './services/context_query_state';
|
||||
import { dataViewMock } from '../../__mocks__/data_view';
|
||||
import { DiscoverGrid } from '../../components/discover_grid/discover_grid';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
import { DiscoverGrid } from '../../components/discover_grid/discover_grid';
|
||||
import { DocTableWrapper } from '../../components/doc_table/doc_table_wrapper';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { buildDataTableRecord } from '../../utils/build_data_record';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
describe('ContextAppContent test', () => {
|
||||
const mountComponent = ({
|
||||
const mountComponent = async ({
|
||||
anchorStatus,
|
||||
isLegacy,
|
||||
}: {
|
||||
|
@ -73,30 +74,35 @@ describe('ContextAppContent test', () => {
|
|||
addFilter: () => {},
|
||||
} as unknown as ContextAppContentProps;
|
||||
|
||||
return mountWithIntl(
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<ContextAppContent {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
await act(async () => {
|
||||
// needed by cell actions to complete async loading
|
||||
component.update();
|
||||
});
|
||||
return component;
|
||||
};
|
||||
|
||||
it('should render legacy table correctly', () => {
|
||||
const component = mountComponent({});
|
||||
it('should render legacy table correctly', async () => {
|
||||
const component = await mountComponent({});
|
||||
expect(component.find(DocTableWrapper).length).toBe(1);
|
||||
const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
|
||||
expect(loadingIndicator.length).toBe(0);
|
||||
expect(component.find(ActionBar).length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders loading indicator', () => {
|
||||
const component = mountComponent({ anchorStatus: LoadingStatus.LOADING });
|
||||
it('renders loading indicator', async () => {
|
||||
const component = await mountComponent({ anchorStatus: LoadingStatus.LOADING });
|
||||
const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator');
|
||||
expect(component.find(DocTableWrapper).length).toBe(1);
|
||||
expect(loadingIndicator.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should render discover grid correctly', () => {
|
||||
const component = mountComponent({ isLegacy: false });
|
||||
it('should render discover grid correctly', async () => {
|
||||
const component = await mountComponent({ isLegacy: false });
|
||||
expect(component.find(DiscoverGrid).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiHorizontalRule, EuiText } from '@elastic/eui';
|
|||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { SortDirection } from '@kbn/data-plugin/public';
|
||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import { CellActionsProvider } from '@kbn/cell-actions';
|
||||
import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../common';
|
||||
import { LoadingStatus } from './services/context_query_state';
|
||||
import { ActionBar } from './components/action_bar/action_bar';
|
||||
|
@ -75,7 +76,7 @@ export function ContextAppContent({
|
|||
setAppState,
|
||||
addFilter,
|
||||
}: ContextAppContentProps) {
|
||||
const { uiSettings: config } = useDiscoverServices();
|
||||
const { uiSettings: config, uiActions } = useDiscoverServices();
|
||||
const services = useDiscoverServices();
|
||||
|
||||
const [expandedDoc, setExpandedDoc] = useState<DataTableRecord | undefined>();
|
||||
|
@ -145,28 +146,30 @@ export function ContextAppContent({
|
|||
)}
|
||||
{!isLegacy && (
|
||||
<div className="dscDocsGrid">
|
||||
<DiscoverGridMemoized
|
||||
ariaLabelledBy="surDocumentsAriaLabel"
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
dataView={dataView}
|
||||
expandedDoc={expandedDoc}
|
||||
isLoading={isAnchorLoading}
|
||||
sampleSize={0}
|
||||
sort={sort as SortOrder[]}
|
||||
isSortEnabled={false}
|
||||
showTimeCol={showTimeCol}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
isPaginationEnabled={false}
|
||||
controlColumnIds={controlColumnIds}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
onFilter={addFilter}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onSetColumns={onSetColumns}
|
||||
DocumentView={DiscoverGridFlyout}
|
||||
services={services}
|
||||
/>
|
||||
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
|
||||
<DiscoverGridMemoized
|
||||
ariaLabelledBy="surDocumentsAriaLabel"
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
dataView={dataView}
|
||||
expandedDoc={expandedDoc}
|
||||
isLoading={isAnchorLoading}
|
||||
sampleSize={0}
|
||||
sort={sort as SortOrder[]}
|
||||
isSortEnabled={false}
|
||||
showTimeCol={showTimeCol}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
isPaginationEnabled={false}
|
||||
controlColumnIds={controlColumnIds}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
onFilter={addFilter}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onSetColumns={onSetColumns}
|
||||
DocumentView={DiscoverGridFlyout}
|
||||
services={services}
|
||||
/>
|
||||
</CellActionsProvider>
|
||||
</div>
|
||||
)}
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { setHeaderActionMenuMounter } from '../../../../kibana_services';
|
||||
|
@ -25,7 +26,7 @@ import { DiscoverAppState } from '../../services/discover_app_state_container';
|
|||
|
||||
setHeaderActionMenuMounter(jest.fn());
|
||||
|
||||
function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
|
||||
async function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
|
||||
const services = discoverServiceMock;
|
||||
services.data.query.timefilter.timefilter.getTime = () => {
|
||||
return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
|
||||
|
@ -46,30 +47,34 @@ function mountComponent(fetchStatus: FetchStatus, hits: EsHitRecord[]) {
|
|||
onFieldEdited: jest.fn(),
|
||||
};
|
||||
|
||||
return mountWithIntl(
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={services}>
|
||||
<DiscoverMainProvider value={stateContainer}>
|
||||
<DiscoverDocuments {...props} />
|
||||
</DiscoverMainProvider>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
await act(async () => {
|
||||
component.update();
|
||||
});
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('Discover documents layout', () => {
|
||||
test('render loading when loading and no documents', () => {
|
||||
const component = mountComponent(FetchStatus.LOADING, []);
|
||||
test('render loading when loading and no documents', async () => {
|
||||
const component = await mountComponent(FetchStatus.LOADING, []);
|
||||
expect(component.find('.dscDocuments__loading').exists()).toBeTruthy();
|
||||
expect(component.find('.dscTable').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('render complete when loading but documents were already fetched', () => {
|
||||
const component = mountComponent(FetchStatus.LOADING, esHits);
|
||||
test('render complete when loading but documents were already fetched', async () => {
|
||||
const component = await mountComponent(FetchStatus.LOADING, esHits);
|
||||
expect(component.find('.dscDocuments__loading').exists()).toBeFalsy();
|
||||
expect(component.find('.dscTable').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('render complete', () => {
|
||||
const component = mountComponent(FetchStatus.COMPLETE, esHits);
|
||||
test('render complete', async () => {
|
||||
const component = await mountComponent(FetchStatus.COMPLETE, esHits);
|
||||
expect(component.find('.dscDocuments__loading').exists()).toBeFalsy();
|
||||
expect(component.find('.dscTable').exists()).toBeTruthy();
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { css } from '@emotion/react';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import { CellActionsProvider } from '@kbn/cell-actions';
|
||||
import { useInternalStateSelector } from '../../services/discover_internal_state_container';
|
||||
import { useAppStateSelector } from '../../services/discover_app_state_container';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
|
@ -85,7 +86,7 @@ function DiscoverDocumentsComponent({
|
|||
const services = useDiscoverServices();
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
const savedSearch = useSavedSearchInitial();
|
||||
const { dataViews, capabilities, uiSettings } = services;
|
||||
const { dataViews, capabilities, uiSettings, uiActions } = services;
|
||||
const [query, sort, rowHeight, rowsPerPage, grid, columns, index] = useAppStateSelector(
|
||||
(state) => {
|
||||
return [
|
||||
|
@ -228,39 +229,43 @@ function DiscoverDocumentsComponent({
|
|||
</DiscoverTourProvider>
|
||||
)}
|
||||
<div className="dscDiscoverGrid">
|
||||
<DataGridMemoized
|
||||
ariaLabelledBy="documentsAriaLabel"
|
||||
columns={currentColumns}
|
||||
expandedDoc={expandedDoc}
|
||||
dataView={dataView}
|
||||
isLoading={isDataLoading}
|
||||
rows={rows}
|
||||
sort={(sort as SortOrder[]) || []}
|
||||
sampleSize={sampleSize}
|
||||
searchDescription={savedSearch.description}
|
||||
searchTitle={savedSearch.title}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
showTimeCol={showTimeCol}
|
||||
settings={grid}
|
||||
onAddColumn={onAddColumn}
|
||||
onFilter={onAddFilter as DocViewFilterFn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onSetColumns={onSetColumns}
|
||||
onSort={!isPlainRecord ? onSort : undefined}
|
||||
onResize={onResizeDataGrid}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
rowHeightState={rowHeight}
|
||||
onUpdateRowHeight={onUpdateRowHeight}
|
||||
isSortEnabled={true}
|
||||
isPlainRecord={isPlainRecord}
|
||||
query={query}
|
||||
rowsPerPageState={rowsPerPage}
|
||||
onUpdateRowsPerPage={onUpdateRowsPerPage}
|
||||
onFieldEdited={onFieldEdited}
|
||||
savedSearchId={savedSearch.id}
|
||||
DocumentView={DiscoverGridFlyout}
|
||||
services={services}
|
||||
/>
|
||||
<CellActionsProvider
|
||||
getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}
|
||||
>
|
||||
<DataGridMemoized
|
||||
ariaLabelledBy="documentsAriaLabel"
|
||||
columns={currentColumns}
|
||||
expandedDoc={expandedDoc}
|
||||
dataView={dataView}
|
||||
isLoading={isDataLoading}
|
||||
rows={rows}
|
||||
sort={(sort as SortOrder[]) || []}
|
||||
sampleSize={sampleSize}
|
||||
searchDescription={savedSearch.description}
|
||||
searchTitle={savedSearch.title}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
showTimeCol={showTimeCol}
|
||||
settings={grid}
|
||||
onAddColumn={onAddColumn}
|
||||
onFilter={onAddFilter as DocViewFilterFn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
onSetColumns={onSetColumns}
|
||||
onSort={!isPlainRecord ? onSort : undefined}
|
||||
onResize={onResizeDataGrid}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
rowHeightState={rowHeight}
|
||||
onUpdateRowHeight={onUpdateRowHeight}
|
||||
isSortEnabled={true}
|
||||
isPlainRecord={isPlainRecord}
|
||||
query={query}
|
||||
rowsPerPageState={rowsPerPage}
|
||||
onUpdateRowsPerPage={onUpdateRowsPerPage}
|
||||
onFieldEdited={onFieldEdited}
|
||||
savedSearchId={savedSearch.id}
|
||||
DocumentView={DiscoverGridFlyout}
|
||||
services={services}
|
||||
/>
|
||||
</CellActionsProvider>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -21,7 +21,8 @@ import {
|
|||
} from '../../services/discover_data_state_container';
|
||||
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { buildDataTableRecord } from '../../../../utils/build_data_record';
|
||||
import { DiscoverHistogramLayout, DiscoverHistogramLayoutProps } from './discover_histogram_layout';
|
||||
import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public';
|
||||
|
@ -141,7 +142,9 @@ const mountComponent = async ({
|
|||
|
||||
// wait for lazy modules
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
||||
component.update();
|
||||
await act(async () => {
|
||||
component.update();
|
||||
});
|
||||
|
||||
return { component, stateContainer };
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { esHits } from '../../../../__mocks__/es_hits';
|
||||
import { dataViewMock } from '../../../../__mocks__/data_view';
|
||||
|
@ -20,7 +21,8 @@ import {
|
|||
} from '../../services/discover_data_state_container';
|
||||
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
|
||||
import { FetchStatus } from '../../../types';
|
||||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { buildDataTableRecord } from '../../../../utils/build_data_record';
|
||||
import { DiscoverMainContent, DiscoverMainContentProps } from './discover_main_content';
|
||||
import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public';
|
||||
|
@ -33,7 +35,7 @@ import { DiscoverMainProvider } from '../../services/discover_state_provider';
|
|||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
const mountComponent = ({
|
||||
const mountComponent = async ({
|
||||
hideChart = false,
|
||||
isPlainRecord = false,
|
||||
viewMode = VIEW_MODE.DOCUMENT_LEVEL,
|
||||
|
@ -116,31 +118,35 @@ const mountComponent = ({
|
|||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
component.update();
|
||||
});
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
describe('Discover main content component', () => {
|
||||
describe('DocumentViewModeToggle', () => {
|
||||
it('should show DocumentViewModeToggle when isPlainRecord is false', async () => {
|
||||
const component = mountComponent();
|
||||
const component = await mountComponent();
|
||||
expect(component.find(DocumentViewModeToggle).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show DocumentViewModeToggle when isPlainRecord is true', async () => {
|
||||
const component = mountComponent({ isPlainRecord: true });
|
||||
const component = await mountComponent({ isPlainRecord: true });
|
||||
expect(component.find(DocumentViewModeToggle).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Document view', () => {
|
||||
it('should show DiscoverDocuments when VIEW_MODE is DOCUMENT_LEVEL', async () => {
|
||||
const component = mountComponent();
|
||||
const component = await mountComponent();
|
||||
expect(component.find(DiscoverDocuments).exists()).toBe(true);
|
||||
expect(component.find(FieldStatisticsTab).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show FieldStatisticsTableMemoized when VIEW_MODE is not DOCUMENT_LEVEL', async () => {
|
||||
const component = mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL });
|
||||
const component = await mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL });
|
||||
expect(component.find(DiscoverDocuments).exists()).toBe(false);
|
||||
expect(component.find(FieldStatisticsTab).exists()).toBe(true);
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plug
|
|||
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { getHistory } from './kibana_services';
|
||||
import { DiscoverStartPlugins } from './plugin';
|
||||
|
@ -103,6 +104,7 @@ export interface DiscoverServices {
|
|||
savedSearch: SavedSearchPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
lens: LensPublicStart;
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export const buildServices = memoize(function (
|
||||
|
@ -159,5 +161,6 @@ export const buildServices = memoize(function (
|
|||
savedSearch: plugins.savedSearch,
|
||||
unifiedSearch: plugins.unifiedSearch,
|
||||
lens: plugins.lens,
|
||||
uiActions: plugins.uiActions,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import { EuiCopy } from '@elastic/eui';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { esHits } from '../../__mocks__/es_hits';
|
||||
import { dataViewMock } from '../../__mocks__/data_view';
|
||||
import { buildDataViewMock, fields } from '../../__mocks__/data_view';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { DiscoverGrid, DiscoverGridProps } from './discover_grid';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
|
@ -20,6 +20,18 @@ import { buildDataTableRecord } from '../../utils/build_data_record';
|
|||
import { getDocId } from '../../utils/get_doc_id';
|
||||
import { EsHitRecord } from '../../types';
|
||||
|
||||
const mockUseDataGridColumnsCellActions = jest.fn((prop: unknown) => []);
|
||||
jest.mock('@kbn/cell-actions', () => ({
|
||||
...jest.requireActual('@kbn/cell-actions'),
|
||||
useDataGridColumnsCellActions: (prop: unknown) => mockUseDataGridColumnsCellActions(prop),
|
||||
}));
|
||||
|
||||
export const dataViewMock = buildDataViewMock({
|
||||
name: 'the-data-view',
|
||||
fields,
|
||||
timeFieldName: '@timestamp',
|
||||
});
|
||||
|
||||
function getProps() {
|
||||
const services = discoverServiceMock;
|
||||
services.dataViewFieldEditor.userPermissions.editIndexPattern = jest.fn().mockReturnValue(true);
|
||||
|
@ -49,14 +61,19 @@ function getProps() {
|
|||
};
|
||||
}
|
||||
|
||||
function getComponent(props: DiscoverGridProps = getProps()) {
|
||||
async function getComponent(props: DiscoverGridProps = getProps()) {
|
||||
const Proxy = (innerProps: DiscoverGridProps) => (
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
<DiscoverGrid {...innerProps} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
return mountWithIntl(<Proxy {...props} />);
|
||||
const component = mountWithIntl(<Proxy {...props} />);
|
||||
await act(async () => {
|
||||
// needed by cell actions to complete async loading
|
||||
component.update();
|
||||
});
|
||||
return component;
|
||||
}
|
||||
|
||||
function getSelectedDocNr(component: ReactWrapper<DiscoverGridProps>) {
|
||||
|
@ -89,10 +106,14 @@ async function toggleDocSelection(
|
|||
}
|
||||
|
||||
describe('DiscoverGrid', () => {
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Document selection', () => {
|
||||
let component: ReactWrapper<DiscoverGridProps>;
|
||||
beforeEach(() => {
|
||||
component = getComponent();
|
||||
beforeEach(async () => {
|
||||
component = await getComponent();
|
||||
});
|
||||
|
||||
test('no documents are selected initially', async () => {
|
||||
|
@ -178,8 +199,8 @@ describe('DiscoverGrid', () => {
|
|||
});
|
||||
|
||||
describe('edit field button', () => {
|
||||
it('should render the edit field button if onFieldEdited is provided', () => {
|
||||
const component = getComponent({
|
||||
it('should render the edit field button if onFieldEdited is provided', async () => {
|
||||
const component = await getComponent({
|
||||
...getProps(),
|
||||
columns: ['message'],
|
||||
onFieldEdited: jest.fn(),
|
||||
|
@ -194,8 +215,8 @@ describe('DiscoverGrid', () => {
|
|||
expect(findTestSubject(component, 'gridEditFieldButton').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render the edit field button if onFieldEdited is not provided', () => {
|
||||
const component = getComponent({
|
||||
it('should not render the edit field button if onFieldEdited is not provided', async () => {
|
||||
const component = await getComponent({
|
||||
...getProps(),
|
||||
columns: ['message'],
|
||||
});
|
||||
|
@ -210,9 +231,91 @@ describe('DiscoverGrid', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cellActionsTriggerId', () => {
|
||||
it('should call useDataGridColumnsCellActions with empty params when no cellActionsTriggerId is provided', async () => {
|
||||
await getComponent({
|
||||
...getProps(),
|
||||
columns: ['message'],
|
||||
onFieldEdited: jest.fn(),
|
||||
});
|
||||
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
triggerId: undefined,
|
||||
getCellValue: expect.any(Function),
|
||||
fields: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call useDataGridColumnsCellActions properly when cellActionsTriggerId defined', async () => {
|
||||
await getComponent({
|
||||
...getProps(),
|
||||
columns: ['message'],
|
||||
onFieldEdited: jest.fn(),
|
||||
cellActionsTriggerId: 'test',
|
||||
});
|
||||
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
triggerId: 'test',
|
||||
getCellValue: expect.any(Function),
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: undefined,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
aggregatable: false,
|
||||
searchable: undefined,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call useDataGridColumnsCellActions with empty field name and type for unsupported field types', async () => {
|
||||
await getComponent({
|
||||
...getProps(),
|
||||
columns: ['message', '_source'],
|
||||
onFieldEdited: jest.fn(),
|
||||
cellActionsTriggerId: 'test',
|
||||
});
|
||||
|
||||
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
triggerId: 'test',
|
||||
getCellValue: expect.any(Function),
|
||||
fields: [
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: undefined,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
aggregatable: false,
|
||||
searchable: undefined,
|
||||
},
|
||||
{
|
||||
searchable: false,
|
||||
aggregatable: false,
|
||||
name: '',
|
||||
type: '',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting', () => {
|
||||
it('should enable in memory sorting with plain records', () => {
|
||||
const component = getComponent({
|
||||
it('should enable in memory sorting with plain records', async () => {
|
||||
const component = await getComponent({
|
||||
...getProps(),
|
||||
columns: ['message'],
|
||||
isPlainRecord: true,
|
||||
|
|
|
@ -25,10 +25,17 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import {
|
||||
useDataGridColumnsCellActions,
|
||||
type UseDataGridColumnsCellActionsProps,
|
||||
type CellActionFieldValue,
|
||||
} from '@kbn/cell-actions';
|
||||
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { ToastsStart, IUiSettingsClient, HttpStart, CoreStart } from '@kbn/core/public';
|
||||
import { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import { KBN_FIELD_TYPES } from '@kbn/data-plugin/common';
|
||||
import { isTypeSupportedByCellActions } from '@kbn/cell-actions/src/utils';
|
||||
import { DocViewFilterFn } from '../../services/doc_views/doc_views_types';
|
||||
import { getSchemaDetectors } from './discover_grid_schema';
|
||||
import { DiscoverGridFlyout } from './discover_grid_flyout';
|
||||
|
@ -53,6 +60,7 @@ import type { DataTableRecord, ValueToStringConverter } from '../../types';
|
|||
import { useRowHeightsOptions } from '../../hooks/use_row_heights_options';
|
||||
import { convertValueToString } from '../../utils/convert_value_to_string';
|
||||
import { getRowsPerPageOptions, getDefaultRowsPerPage } from '../../utils/rows_per_page';
|
||||
import { convertCellActionValue } from './discover_grid_cell_actions';
|
||||
|
||||
const themeDefault = { darkMode: false };
|
||||
|
||||
|
@ -203,6 +211,10 @@ export interface DiscoverGridProps {
|
|||
* Document detail view component
|
||||
*/
|
||||
DocumentView?: typeof DiscoverGridFlyout;
|
||||
/**
|
||||
* Optional triggerId to retrieve the column cell actions that will override the default ones
|
||||
*/
|
||||
cellActionsTriggerId?: string;
|
||||
/**
|
||||
* Service dependencies
|
||||
*/
|
||||
|
@ -248,6 +260,7 @@ export const DiscoverGrid = ({
|
|||
isSortEnabled = true,
|
||||
isPaginationEnabled = true,
|
||||
controlColumnIds = CONTROL_COLUMN_IDS_DEFAULT,
|
||||
cellActionsTriggerId,
|
||||
className,
|
||||
rowHeightState,
|
||||
onUpdateRowHeight,
|
||||
|
@ -432,14 +445,57 @@ export const DiscoverGrid = ({
|
|||
[dataView, onFieldEdited, services.dataViewFieldEditor]
|
||||
);
|
||||
|
||||
const visibleColumns = useMemo(
|
||||
() => getVisibleColumns(displayedColumns, dataView, showTimeCol) as string[],
|
||||
[dataView, displayedColumns, showTimeCol]
|
||||
);
|
||||
|
||||
const getCellValue = useCallback<UseDataGridColumnsCellActionsProps['getCellValue']>(
|
||||
(fieldName, rowIndex): CellActionFieldValue =>
|
||||
convertCellActionValue(displayedRows[rowIndex % displayedRows.length].flattened[fieldName]),
|
||||
[displayedRows]
|
||||
);
|
||||
|
||||
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
|
||||
() =>
|
||||
cellActionsTriggerId && !isPlainRecord
|
||||
? visibleColumns.map((columnName) => {
|
||||
const field = dataView.getFieldByName(columnName);
|
||||
if (!field || !isTypeSupportedByCellActions(field.type as KBN_FIELD_TYPES)) {
|
||||
// disable custom actions on object columns
|
||||
return {
|
||||
name: '',
|
||||
type: '',
|
||||
aggregatable: false,
|
||||
searchable: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: columnName,
|
||||
type: field.type,
|
||||
aggregatable: field.aggregatable,
|
||||
searchable: field.searchable,
|
||||
};
|
||||
})
|
||||
: undefined,
|
||||
[cellActionsTriggerId, isPlainRecord, visibleColumns, dataView]
|
||||
);
|
||||
|
||||
const columnsCellActions = useDataGridColumnsCellActions({
|
||||
fields: cellActionsFields,
|
||||
getCellValue,
|
||||
triggerId: cellActionsTriggerId,
|
||||
dataGridRef,
|
||||
});
|
||||
|
||||
const euiGridColumns = useMemo(
|
||||
() =>
|
||||
getEuiGridColumns({
|
||||
columns: displayedColumns,
|
||||
columns: visibleColumns,
|
||||
columnsCellActions,
|
||||
rowsCount: displayedRows.length,
|
||||
settings,
|
||||
dataView,
|
||||
showTimeCol,
|
||||
defaultColumns,
|
||||
isSortEnabled,
|
||||
isPlainRecord,
|
||||
|
@ -454,10 +510,10 @@ export const DiscoverGrid = ({
|
|||
}),
|
||||
[
|
||||
onFilter,
|
||||
displayedColumns,
|
||||
visibleColumns,
|
||||
columnsCellActions,
|
||||
displayedRows,
|
||||
dataView,
|
||||
showTimeCol,
|
||||
settings,
|
||||
defaultColumns,
|
||||
isSortEnabled,
|
||||
|
@ -477,12 +533,12 @@ export const DiscoverGrid = ({
|
|||
const schemaDetectors = useMemo(() => getSchemaDetectors(), []);
|
||||
const columnsVisibility = useMemo(
|
||||
() => ({
|
||||
visibleColumns: getVisibleColumns(displayedColumns, dataView, showTimeCol) as string[],
|
||||
visibleColumns,
|
||||
setVisibleColumns: (newColumns: string[]) => {
|
||||
onSetColumns(newColumns, hideTimeColumn);
|
||||
},
|
||||
}),
|
||||
[displayedColumns, dataView, showTimeCol, hideTimeColumn, onSetColumns]
|
||||
[visibleColumns, hideTimeColumn, onSetColumns]
|
||||
);
|
||||
const sorting = useMemo(() => {
|
||||
if (isSortEnabled) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, { useContext } from 'react';
|
|||
import { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { CellActionFieldValue } from '@kbn/cell-actions';
|
||||
import { DocViewFilterFn } from '../../services/doc_views/doc_views_types';
|
||||
import { DiscoverGridContext, GridContext } from './discover_grid_context';
|
||||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
|
@ -120,3 +121,12 @@ export const CopyBtn = ({ Component, rowIndex, columnId }: EuiDataGridColumnCell
|
|||
export function buildCellActions(field: DataViewField, onFilter?: DocViewFilterFn) {
|
||||
return [...(onFilter && field.filterable ? [FilterInBtn, FilterOutBtn] : []), CopyBtn];
|
||||
}
|
||||
|
||||
// Converts the cell action value to the type expected by CellActions component
|
||||
export const convertCellActionValue = (rawValue: unknown): CellActionFieldValue => {
|
||||
const value = rawValue as CellActionFieldValue | number | number[];
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((val) => (val != null ? val.toString() : val));
|
||||
}
|
||||
return value != null ? value.toString() : value;
|
||||
};
|
||||
|
|
|
@ -7,152 +7,39 @@
|
|||
*/
|
||||
|
||||
import { dataViewMock } from '../../__mocks__/data_view';
|
||||
import { getEuiGridColumns } from './discover_grid_columns';
|
||||
import { getEuiGridColumns, getVisibleColumns } from './discover_grid_columns';
|
||||
import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield';
|
||||
import { discoverGridContextMock } from '../../__mocks__/grid_context';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
|
||||
const columns = ['extension', 'message'];
|
||||
const columnsWithTimeCol = getVisibleColumns(
|
||||
['extension', 'message'],
|
||||
dataViewWithTimefieldMock,
|
||||
true
|
||||
) as string[];
|
||||
|
||||
describe('Discover grid columns', function () {
|
||||
it('returns eui grid columns without time column', async () => {
|
||||
const actual = getEuiGridColumns({
|
||||
columns: ['extension', 'message'],
|
||||
settings: {},
|
||||
dataView: dataViewMock,
|
||||
showTimeCol: false,
|
||||
defaultColumns: false,
|
||||
isSortEnabled: true,
|
||||
isPlainRecord: false,
|
||||
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
|
||||
rowsCount: 100,
|
||||
services: {
|
||||
uiSettings: discoverServiceMock.uiSettings,
|
||||
toastNotifications: discoverServiceMock.toastNotifications,
|
||||
},
|
||||
hasEditDataViewPermission: () =>
|
||||
discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"displayAsText": "extension",
|
||||
"id": "extension",
|
||||
"isSortable": false,
|
||||
"schema": "string",
|
||||
describe('getEuiGridColumns', () => {
|
||||
it('returns eui grid columns showing default columns', async () => {
|
||||
const actual = getEuiGridColumns({
|
||||
columns,
|
||||
settings: {},
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
defaultColumns: true,
|
||||
isSortEnabled: true,
|
||||
isPlainRecord: false,
|
||||
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
|
||||
rowsCount: 100,
|
||||
services: {
|
||||
uiSettings: discoverServiceMock.uiSettings,
|
||||
toastNotifications: discoverServiceMock.toastNotifications,
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
],
|
||||
"displayAsText": "message",
|
||||
"id": "message",
|
||||
"isSortable": false,
|
||||
"schema": "string",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('returns eui grid columns without time column showing default columns', async () => {
|
||||
const actual = getEuiGridColumns({
|
||||
columns: ['extension', 'message'],
|
||||
settings: {},
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
showTimeCol: false,
|
||||
defaultColumns: true,
|
||||
isSortEnabled: true,
|
||||
isPlainRecord: false,
|
||||
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
|
||||
rowsCount: 100,
|
||||
services: {
|
||||
uiSettings: discoverServiceMock.uiSettings,
|
||||
toastNotifications: discoverServiceMock.toastNotifications,
|
||||
},
|
||||
hasEditDataViewPermission: () =>
|
||||
discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
hasEditDataViewPermission: () =>
|
||||
discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
|
@ -246,27 +133,27 @@ describe('Discover grid columns', function () {
|
|||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
it('returns eui grid columns with time column', async () => {
|
||||
const actual = getEuiGridColumns({
|
||||
columns: ['extension', 'message'],
|
||||
settings: {},
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
showTimeCol: true,
|
||||
defaultColumns: false,
|
||||
isSortEnabled: true,
|
||||
isPlainRecord: false,
|
||||
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
|
||||
rowsCount: 100,
|
||||
services: {
|
||||
uiSettings: discoverServiceMock.uiSettings,
|
||||
toastNotifications: discoverServiceMock.toastNotifications,
|
||||
},
|
||||
hasEditDataViewPermission: () =>
|
||||
discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
|
||||
it('returns eui grid columns with time column', async () => {
|
||||
const actual = getEuiGridColumns({
|
||||
columns: columnsWithTimeCol,
|
||||
settings: {},
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
defaultColumns: false,
|
||||
isSortEnabled: true,
|
||||
isPlainRecord: false,
|
||||
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
|
||||
rowsCount: 100,
|
||||
services: {
|
||||
uiSettings: discoverServiceMock.uiSettings,
|
||||
toastNotifications: discoverServiceMock.toastNotifications,
|
||||
},
|
||||
hasEditDataViewPermission: () =>
|
||||
discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
|
@ -431,191 +318,216 @@ describe('Discover grid columns', function () {
|
|||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns eui grid with in memory sorting', async () => {
|
||||
const actual = getEuiGridColumns({
|
||||
columns: columnsWithTimeCol,
|
||||
settings: {},
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
defaultColumns: false,
|
||||
isSortEnabled: true,
|
||||
isPlainRecord: true,
|
||||
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
|
||||
rowsCount: 100,
|
||||
services: {
|
||||
uiSettings: discoverServiceMock.uiSettings,
|
||||
toastNotifications: discoverServiceMock.toastNotifications,
|
||||
},
|
||||
hasEditDataViewPermission: () =>
|
||||
discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": false,
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"display": <div
|
||||
aria-label="timestamp - this field represents the time that events occurred."
|
||||
>
|
||||
<EuiToolTip
|
||||
content="This field represents the time that events occurred."
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<React.Fragment>
|
||||
timestamp
|
||||
|
||||
<EuiIcon
|
||||
type="clock"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiToolTip>
|
||||
</div>,
|
||||
"displayAsText": "timestamp",
|
||||
"id": "timestamp",
|
||||
"initialWidth": 210,
|
||||
"isSortable": true,
|
||||
"schema": "datetime",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"displayAsText": "extension",
|
||||
"id": "extension",
|
||||
"isSortable": true,
|
||||
"schema": "string",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
],
|
||||
"displayAsText": "message",
|
||||
"id": "message",
|
||||
"isSortable": true,
|
||||
"schema": "string",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns eui grid with inmemory sorting', async () => {
|
||||
const actual = getEuiGridColumns({
|
||||
columns: ['extension', 'message'],
|
||||
settings: {},
|
||||
dataView: dataViewWithTimefieldMock,
|
||||
showTimeCol: true,
|
||||
defaultColumns: false,
|
||||
isSortEnabled: true,
|
||||
isPlainRecord: true,
|
||||
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
|
||||
rowsCount: 100,
|
||||
services: {
|
||||
uiSettings: discoverServiceMock.uiSettings,
|
||||
toastNotifications: discoverServiceMock.toastNotifications,
|
||||
},
|
||||
hasEditDataViewPermission: () =>
|
||||
discoverServiceMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
|
||||
onFilter: () => {},
|
||||
describe('getVisibleColumns', () => {
|
||||
it('returns grid columns without time column when data view has no timestamp field', () => {
|
||||
const actual = getVisibleColumns(['extension', 'message'], dataViewMock, true) as string[];
|
||||
expect(actual).toEqual(['extension', 'message']);
|
||||
});
|
||||
|
||||
it('returns grid columns without time column when showTimeCol is falsy', () => {
|
||||
const actual = getVisibleColumns(
|
||||
['extension', 'message'],
|
||||
dataViewWithTimefieldMock,
|
||||
false
|
||||
) as string[];
|
||||
expect(actual).toEqual(['extension', 'message']);
|
||||
});
|
||||
|
||||
it('returns grid columns with time column when data view has timestamp field', () => {
|
||||
const actual = getVisibleColumns(
|
||||
['extension', 'message'],
|
||||
dataViewWithTimefieldMock,
|
||||
true
|
||||
) as string[];
|
||||
expect(actual).toEqual(['timestamp', 'extension', 'message']);
|
||||
});
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": false,
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"display": <div
|
||||
aria-label="timestamp - this field represents the time that events occurred."
|
||||
>
|
||||
<EuiToolTip
|
||||
content="This field represents the time that events occurred."
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<React.Fragment>
|
||||
timestamp
|
||||
|
||||
<EuiIcon
|
||||
type="clock"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiToolTip>
|
||||
</div>,
|
||||
"displayAsText": "timestamp",
|
||||
"id": "timestamp",
|
||||
"initialWidth": 210,
|
||||
"isSortable": true,
|
||||
"schema": "datetime",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
[Function],
|
||||
[Function],
|
||||
],
|
||||
"displayAsText": "extension",
|
||||
"id": "extension",
|
||||
"isSortable": true,
|
||||
"schema": "string",
|
||||
},
|
||||
Object {
|
||||
"actions": Object {
|
||||
"additional": Array [
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnNameToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy name"
|
||||
id="discover.grid.copyColumnNameToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
Object {
|
||||
"data-test-subj": "gridCopyColumnValuesToClipBoardButton",
|
||||
"iconProps": Object {
|
||||
"size": "m",
|
||||
},
|
||||
"iconType": "copyClipboard",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Copy column"
|
||||
id="discover.grid.copyColumnValuesToClipBoardButton"
|
||||
values={Object {}}
|
||||
/>,
|
||||
"onClick": [Function],
|
||||
"size": "xs",
|
||||
},
|
||||
],
|
||||
"showHide": Object {
|
||||
"iconType": "cross",
|
||||
"label": "Remove column",
|
||||
},
|
||||
"showMoveLeft": true,
|
||||
"showMoveRight": true,
|
||||
},
|
||||
"cellActions": Array [
|
||||
[Function],
|
||||
],
|
||||
"displayAsText": "message",
|
||||
"id": "message",
|
||||
"isSortable": true,
|
||||
"schema": "string",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiDataGridColumn, EuiIcon, EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui';
|
||||
import {
|
||||
type EuiDataGridColumn,
|
||||
type EuiDataGridColumnCellAction,
|
||||
EuiIcon,
|
||||
EuiScreenReaderOnly,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { ToastsStart, IUiSettingsClient } from '@kbn/core/public';
|
||||
import { DocViewFilterFn } from '../../services/doc_views/doc_views_types';
|
||||
|
@ -72,6 +78,7 @@ function buildEuiGridColumn({
|
|||
rowsCount,
|
||||
onFilter,
|
||||
editField,
|
||||
columnCellActions,
|
||||
}: {
|
||||
columnName: string;
|
||||
columnWidth: number | undefined;
|
||||
|
@ -85,6 +92,7 @@ function buildEuiGridColumn({
|
|||
rowsCount: number;
|
||||
onFilter?: DocViewFilterFn;
|
||||
editField?: (fieldName: string) => void;
|
||||
columnCellActions?: EuiDataGridColumnCellAction[];
|
||||
}) {
|
||||
const dataViewField = dataView.getFieldByName(columnName);
|
||||
const editFieldButton =
|
||||
|
@ -98,6 +106,13 @@ function buildEuiGridColumn({
|
|||
})
|
||||
: dataViewField?.displayName || columnName;
|
||||
|
||||
let cellActions: EuiDataGridColumnCellAction[];
|
||||
if (columnCellActions?.length) {
|
||||
cellActions = columnCellActions;
|
||||
} else {
|
||||
cellActions = dataViewField ? buildCellActions(dataViewField, onFilter) : [];
|
||||
}
|
||||
|
||||
const column: EuiDataGridColumn = {
|
||||
id: columnName,
|
||||
schema: getSchemaByKbnType(dataViewField?.type),
|
||||
|
@ -134,7 +149,7 @@ function buildEuiGridColumn({
|
|||
...(editFieldButton ? [editFieldButton] : []),
|
||||
],
|
||||
},
|
||||
cellActions: dataViewField ? buildCellActions(dataViewField, onFilter) : [],
|
||||
cellActions,
|
||||
};
|
||||
|
||||
if (column.id === dataView.timeFieldName) {
|
||||
|
@ -172,10 +187,10 @@ function buildEuiGridColumn({
|
|||
|
||||
export function getEuiGridColumns({
|
||||
columns,
|
||||
columnsCellActions,
|
||||
rowsCount,
|
||||
settings,
|
||||
dataView,
|
||||
showTimeCol,
|
||||
defaultColumns,
|
||||
isSortEnabled,
|
||||
isPlainRecord,
|
||||
|
@ -186,10 +201,10 @@ export function getEuiGridColumns({
|
|||
editField,
|
||||
}: {
|
||||
columns: string[];
|
||||
columnsCellActions?: EuiDataGridColumnCellAction[][];
|
||||
rowsCount: number;
|
||||
settings: DiscoverGridSettings | undefined;
|
||||
dataView: DataView;
|
||||
showTimeCol: boolean;
|
||||
defaultColumns: boolean;
|
||||
isSortEnabled: boolean;
|
||||
isPlainRecord?: boolean;
|
||||
|
@ -202,17 +217,12 @@ export function getEuiGridColumns({
|
|||
onFilter: DocViewFilterFn;
|
||||
editField?: (fieldName: string) => void;
|
||||
}) {
|
||||
const timeFieldName = dataView.timeFieldName;
|
||||
const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0;
|
||||
|
||||
let visibleColumns = columns;
|
||||
if (showTimeCol && dataView.timeFieldName && !columns.find((col) => col === timeFieldName)) {
|
||||
visibleColumns = [dataView.timeFieldName, ...columns];
|
||||
}
|
||||
|
||||
return visibleColumns.map((column) =>
|
||||
return columns.map((column, columnIndex) =>
|
||||
buildEuiGridColumn({
|
||||
columnName: column,
|
||||
columnCellActions: columnsCellActions?.[columnIndex],
|
||||
columnWidth: getColWidth(column),
|
||||
dataView,
|
||||
defaultColumns,
|
||||
|
@ -231,7 +241,7 @@ export function getEuiGridColumns({
|
|||
export function getVisibleColumns(columns: string[], dataView: DataView, showTimeCol: boolean) {
|
||||
const timeFieldName = dataView.timeFieldName;
|
||||
|
||||
if (showTimeCol && !columns.find((col) => col === timeFieldName)) {
|
||||
if (showTimeCol && timeFieldName && !columns.find((col) => col === timeFieldName)) {
|
||||
return [timeFieldName, ...columns];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,4 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { Trigger } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
export { SEARCH_EMBEDDABLE_TYPE } from '../../common';
|
||||
|
||||
export const SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID =
|
||||
'SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID';
|
||||
|
||||
export const SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER: Trigger = {
|
||||
id: SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID,
|
||||
title: 'Discover saved searches embeddable cell actions',
|
||||
description:
|
||||
'This trigger is used to replace the cell actions for Discover saved search embeddable grid.',
|
||||
} as const;
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { SEARCH_EMBEDDABLE_TYPE } from './constants';
|
||||
export { SEARCH_EMBEDDABLE_TYPE, SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from './constants';
|
||||
export * from './types';
|
||||
export * from './search_embeddable_factory';
|
||||
|
|
|
@ -35,12 +35,13 @@ import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
|||
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { CellActionsProvider } from '@kbn/cell-actions';
|
||||
import { VIEW_MODE } from '../../common/constants';
|
||||
import { getSortForEmbeddable, SortPair } from '../utils/sorting';
|
||||
import { buildDataTableRecord } from '../utils/build_data_record';
|
||||
import { DataTableRecord, EsHitRecord } from '../types';
|
||||
import { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
|
||||
import { SEARCH_EMBEDDABLE_TYPE } from './constants';
|
||||
import { SEARCH_EMBEDDABLE_TYPE, SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from './constants';
|
||||
import { DiscoverServices } from '../build_services';
|
||||
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
|
||||
import {
|
||||
|
@ -399,6 +400,7 @@ export class SavedSearchEmbeddable
|
|||
onUpdateRowsPerPage: (rowsPerPage) => {
|
||||
this.updateInput({ rowsPerPage });
|
||||
},
|
||||
cellActionsTriggerId: SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID,
|
||||
};
|
||||
|
||||
const timeRangeSearchSource = searchSource.create();
|
||||
|
@ -565,11 +567,14 @@ export class SavedSearchEmbeddable
|
|||
query,
|
||||
};
|
||||
if (searchProps.services) {
|
||||
const { getTriggerCompatibleActions } = searchProps.services.uiActions;
|
||||
ReactDOM.render(
|
||||
<I18nProvider>
|
||||
<KibanaThemeProvider theme$={searchProps.services.core.theme.theme$}>
|
||||
<KibanaContextProvider services={searchProps.services}>
|
||||
<SavedSearchEmbeddableComponent {...props} />
|
||||
<CellActionsProvider getTriggerCompatibleActions={getTriggerCompatibleActions}>
|
||||
<SavedSearchEmbeddableComponent {...props} />
|
||||
</CellActionsProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaThemeProvider>
|
||||
</I18nProvider>,
|
||||
|
|
|
@ -15,5 +15,5 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
export type { ISearchEmbeddable, SearchInput } from './embeddable';
|
||||
export { SEARCH_EMBEDDABLE_TYPE } from './embeddable';
|
||||
export { SEARCH_EMBEDDABLE_TYPE, SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from './embeddable';
|
||||
export { loadSharingDataHelpers } from './utils';
|
||||
|
|
|
@ -74,6 +74,7 @@ import {
|
|||
import { DiscoverAppLocator, DiscoverAppLocatorDefinition } from '../common';
|
||||
import type { CustomizationCallback } from './customizations';
|
||||
import { createCustomizeFunction, createProfileRegistry } from './customizations/profile_registry';
|
||||
import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER } from './embeddable/constants';
|
||||
|
||||
const DocViewerLegacyTable = React.lazy(
|
||||
() => import('./services/doc_views/components/doc_viewer_table/legacy')
|
||||
|
@ -405,6 +406,8 @@ export class DiscoverPlugin
|
|||
|
||||
const { uiActions } = plugins;
|
||||
|
||||
uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER);
|
||||
|
||||
const viewSavedSearchAction = new ViewSavedSearchAction(core.application);
|
||||
uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction);
|
||||
setUiActions(plugins.uiActions);
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
"@kbn/dom-drag-drop",
|
||||
"@kbn/unified-field-list",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/cell-actions",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -171,20 +171,17 @@ describe('DataTable', () => {
|
|||
|
||||
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({
|
||||
triggerId: 'mockCellActionsTrigger',
|
||||
data: [
|
||||
fields: [
|
||||
{
|
||||
values: [data[0]?.data[0]?.value],
|
||||
field: {
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
esTypes: ['date'],
|
||||
searchable: true,
|
||||
subType: undefined,
|
||||
},
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
esTypes: ['date'],
|
||||
searchable: true,
|
||||
subType: undefined,
|
||||
},
|
||||
],
|
||||
|
||||
getCellValue: expect.any(Function),
|
||||
metadata: {
|
||||
scopeId: 'table-test',
|
||||
},
|
||||
|
@ -202,7 +199,8 @@ describe('DataTable', () => {
|
|||
|
||||
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: [],
|
||||
triggerId: undefined,
|
||||
fields: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -38,7 +38,10 @@ import {
|
|||
DeprecatedRowRenderer,
|
||||
TimelineItem,
|
||||
} from '@kbn/timelines-plugin/common';
|
||||
import { useDataGridColumnsCellActions } from '@kbn/cell-actions';
|
||||
import {
|
||||
useDataGridColumnsCellActions,
|
||||
UseDataGridColumnsCellActionsProps,
|
||||
} from '@kbn/cell-actions';
|
||||
import { DataTableModel, DataTableState } from '../../store/data_table/types';
|
||||
|
||||
import { getColumnHeader, getColumnHeaders } from './column_headers/helpers';
|
||||
|
@ -327,36 +330,39 @@ export const DataTableComponent = React.memo<DataTableProps>(
|
|||
[dispatch, id]
|
||||
);
|
||||
|
||||
const columnsCellActionsProps = useMemo(() => {
|
||||
const columnsCellActionData = !cellActionsTriggerId
|
||||
? []
|
||||
: columnHeaders.map((column) => ({
|
||||
// TODO use FieldSpec object instead of column
|
||||
field: {
|
||||
const cellActionsMetadata = useMemo(() => ({ scopeId: id }), [id]);
|
||||
|
||||
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
|
||||
() =>
|
||||
cellActionsTriggerId
|
||||
? // TODO use FieldSpec object instead of column
|
||||
columnHeaders.map((column) => ({
|
||||
name: column.id,
|
||||
type: column.type ?? 'keyword',
|
||||
aggregatable: column.aggregatable ?? false,
|
||||
searchable: column.searchable ?? false,
|
||||
esTypes: column.esTypes ?? [],
|
||||
subType: column.subType,
|
||||
},
|
||||
values: data.map(
|
||||
({ data: columnData }) =>
|
||||
columnData.find((rowData) => rowData.field === column.id)?.value
|
||||
),
|
||||
}));
|
||||
}))
|
||||
: undefined,
|
||||
[cellActionsTriggerId, columnHeaders]
|
||||
);
|
||||
|
||||
return {
|
||||
triggerId: cellActionsTriggerId || '',
|
||||
data: columnsCellActionData,
|
||||
metadata: {
|
||||
scopeId: id,
|
||||
},
|
||||
dataGridRef,
|
||||
};
|
||||
}, [columnHeaders, cellActionsTriggerId, id, data]);
|
||||
const getCellValue = useCallback<UseDataGridColumnsCellActionsProps['getCellValue']>(
|
||||
(fieldName, rowIndex) => {
|
||||
const pageIndex = rowIndex % data.length;
|
||||
return data[pageIndex].data.find((rowData) => rowData.field === fieldName)?.value;
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
const columnsCellActions = useDataGridColumnsCellActions(columnsCellActionsProps);
|
||||
const columnsCellActions = useDataGridColumnsCellActions({
|
||||
triggerId: cellActionsTriggerId,
|
||||
fields: cellActionsFields,
|
||||
getCellValue,
|
||||
metadata: cellActionsMetadata,
|
||||
dataGridRef,
|
||||
});
|
||||
|
||||
const columnsWithCellActions: EuiDataGridColumn[] = useMemo(
|
||||
() =>
|
||||
|
@ -469,5 +475,3 @@ export const DataTableComponent = React.memo<DataTableProps>(
|
|||
);
|
||||
}
|
||||
);
|
||||
|
||||
DataTableComponent.displayName = 'DataTableComponent';
|
||||
|
|
|
@ -19,8 +19,9 @@
|
|||
"cloudSecurityPosture",
|
||||
"dashboard",
|
||||
"data",
|
||||
"ecsDataQualityDashboard",
|
||||
"dataViews",
|
||||
"discover",
|
||||
"ecsDataQualityDashboard",
|
||||
"embeddable",
|
||||
"eventLog",
|
||||
"features",
|
||||
|
@ -41,7 +42,6 @@
|
|||
"unifiedSearch",
|
||||
"files",
|
||||
"controls",
|
||||
"dataViews",
|
||||
"savedObjectsManagement",
|
||||
"stackConnectors",
|
||||
],
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 type { SecurityAppStore } from '../../../common/store/types';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { addProvider } from '../../../timelines/store/timeline/actions';
|
||||
import { createAddToTimelineDiscoverCellActionFactory } from './add_to_timeline';
|
||||
import type { CellActionExecutionContext } from '@kbn/cell-actions';
|
||||
import { GEO_FIELD_TYPE } from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import { APP_UI_ID } from '../../../../common';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockWarningToast = services.notifications.toasts.addWarning;
|
||||
|
||||
const currentAppIdSubject$ = new BehaviorSubject<string>(APP_UI_ID);
|
||||
services.application.currentAppId$ = currentAppIdSubject$.asObservable();
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
const store = {
|
||||
dispatch: mockDispatch,
|
||||
} as unknown as SecurityAppStore;
|
||||
|
||||
const value = 'the-value';
|
||||
|
||||
const context = {
|
||||
data: [{ field: { name: 'user.name', type: 'text' }, value }],
|
||||
} as CellActionExecutionContext;
|
||||
|
||||
const defaultDataProvider = {
|
||||
type: addProvider.type,
|
||||
payload: {
|
||||
id: TimelineId.active,
|
||||
providers: [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
id: 'event-field-default-timeline-1-user_name-0-the-value',
|
||||
kqlQuery: '',
|
||||
name: 'user.name',
|
||||
queryMatch: {
|
||||
field: 'user.name',
|
||||
operator: ':',
|
||||
value: 'the-value',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('createAddToTimelineDiscoverCellActionFactory', () => {
|
||||
const addToTimelineDiscoverCellActionFactory = createAddToTimelineDiscoverCellActionFactory({
|
||||
store,
|
||||
services,
|
||||
});
|
||||
const addToTimelineAction = addToTimelineDiscoverCellActionFactory({
|
||||
id: 'testAddToTimeline',
|
||||
order: 1,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
currentAppIdSubject$.next(APP_UI_ID);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return display name', () => {
|
||||
expect(addToTimelineAction.getDisplayName(context)).toEqual('Add to timeline');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(addToTimelineAction.getIconType(context)).toEqual('timeline');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return true if everything is okay', async () => {
|
||||
expect(await addToTimelineAction.isCompatible(context)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if not in security', async () => {
|
||||
currentAppIdSubject$.next('not-security');
|
||||
expect(await addToTimelineAction.isCompatible(context)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if field not allowed', async () => {
|
||||
expect(
|
||||
await addToTimelineAction.isCompatible({
|
||||
...context,
|
||||
data: [
|
||||
{
|
||||
...context.data[0],
|
||||
field: { ...context.data[0].field, name: 'signal.reason' },
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute normally', async () => {
|
||||
await addToTimelineAction.execute(context);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(defaultDataProvider);
|
||||
expect(mockWarningToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show warning if no provider added', async () => {
|
||||
await addToTimelineAction.execute({
|
||||
...context,
|
||||
data: [
|
||||
{
|
||||
...context.data[0],
|
||||
field: {
|
||||
...context.data[0].field,
|
||||
type: GEO_FIELD_TYPE,
|
||||
},
|
||||
value,
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockWarningToast).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 type { CellAction, CellActionFactory } from '@kbn/cell-actions';
|
||||
import type { SecurityAppStore } from '../../../common/store';
|
||||
import { isInSecurityApp } from '../../utils';
|
||||
import type { StartServices } from '../../../types';
|
||||
import { createAddToTimelineCellActionFactory } from '../cell_action/add_to_timeline';
|
||||
|
||||
export const createAddToTimelineDiscoverCellActionFactory = ({
|
||||
store,
|
||||
services,
|
||||
}: {
|
||||
store: SecurityAppStore;
|
||||
services: StartServices;
|
||||
}): CellActionFactory<CellAction> => {
|
||||
const { application } = services;
|
||||
|
||||
let currentAppId: string | undefined;
|
||||
application.currentAppId$.subscribe((appId) => {
|
||||
currentAppId = appId;
|
||||
});
|
||||
|
||||
const securityAddToTimelineActionFactory = createAddToTimelineCellActionFactory({
|
||||
store,
|
||||
services,
|
||||
});
|
||||
|
||||
return securityAddToTimelineActionFactory.combine<CellAction>({
|
||||
isCompatible: async () => isInSecurityApp(currentAppId),
|
||||
});
|
||||
};
|
|
@ -8,3 +8,4 @@
|
|||
export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline';
|
||||
export { createInvestigateInNewTimelineCellActionFactory } from './cell_action/investigate_in_new_timeline';
|
||||
export { createAddToTimelineLensAction } from './lens/add_to_timeline';
|
||||
export { createAddToTimelineDiscoverCellActionFactory } from './discover/add_to_timeline';
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { createCopyToClipboardCellActionFactory } from './copy_to_clipboard';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import type { CellActionExecutionContext } from '@kbn/cell-actions';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockSuccessToast = services.notifications.toasts.addSuccess;
|
||||
|
||||
const mockCopy = jest.fn((text: string) => true);
|
||||
jest.mock('copy-to-clipboard', () => (text: string) => mockCopy(text));
|
||||
|
||||
describe('createCopyToClipboardCellActionFactory', () => {
|
||||
const copyToClipboardActionFactory = createCopyToClipboardCellActionFactory({ services });
|
||||
const copyToClipboardAction = copyToClipboardActionFactory({ id: 'testAction' });
|
||||
const context = {
|
||||
data: [{ field: { name: 'user.name', type: 'text' }, value: 'the value' }],
|
||||
} as CellActionExecutionContext;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return display name', () => {
|
||||
expect(copyToClipboardAction.getDisplayName(context)).toEqual('Copy to Clipboard');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(copyToClipboardAction.getIconType(context)).toEqual('copyClipboard');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return true if everything is okay', async () => {
|
||||
expect(await copyToClipboardAction.isCompatible(context)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if field not allowed', async () => {
|
||||
expect(
|
||||
await copyToClipboardAction.isCompatible({
|
||||
...context,
|
||||
data: [
|
||||
{
|
||||
...context.data[0].field,
|
||||
field: { ...context.data[0].field, name: 'signal.reason' },
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute normally', async () => {
|
||||
await copyToClipboardAction.execute(context);
|
||||
expect(mockCopy).toHaveBeenCalledWith('user.name: "the value"');
|
||||
expect(mockSuccessToast).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { createCopyToClipboardDiscoverCellActionFactory } from './copy_to_clipboard';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import type { CellActionExecutionContext } from '@kbn/cell-actions';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { APP_UI_ID } from '../../../../common';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockSuccessToast = services.notifications.toasts.addSuccess;
|
||||
|
||||
const currentAppIdSubject$ = new BehaviorSubject<string>(APP_UI_ID);
|
||||
services.application.currentAppId$ = currentAppIdSubject$.asObservable();
|
||||
|
||||
const mockCopy = jest.fn((text: string) => true);
|
||||
jest.mock('copy-to-clipboard', () => (text: string) => mockCopy(text));
|
||||
|
||||
describe('createCopyToClipboardDiscoverCellActionFactory', () => {
|
||||
const copyToClipboardActionFactory = createCopyToClipboardDiscoverCellActionFactory({ services });
|
||||
const copyToClipboardAction = copyToClipboardActionFactory({ id: 'testAction' });
|
||||
const context = {
|
||||
data: [
|
||||
{
|
||||
field: { name: 'user.name', type: 'text' },
|
||||
value: 'the value',
|
||||
},
|
||||
],
|
||||
} as CellActionExecutionContext;
|
||||
|
||||
beforeEach(() => {
|
||||
currentAppIdSubject$.next(APP_UI_ID);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return display name', () => {
|
||||
expect(copyToClipboardAction.getDisplayName(context)).toEqual('Copy to Clipboard');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(copyToClipboardAction.getIconType(context)).toEqual('copyClipboard');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return true if everything is okay', async () => {
|
||||
expect(await copyToClipboardAction.isCompatible(context)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if not in security', async () => {
|
||||
currentAppIdSubject$.next('not-security');
|
||||
expect(await copyToClipboardAction.isCompatible(context)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if field not allowed', async () => {
|
||||
expect(
|
||||
await copyToClipboardAction.isCompatible({
|
||||
...context,
|
||||
data: [
|
||||
{
|
||||
...context.data[0],
|
||||
field: { ...context.data[0].field, name: 'signal.reason' },
|
||||
},
|
||||
],
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should execute normally', async () => {
|
||||
await copyToClipboardAction.execute(context);
|
||||
expect(mockCopy).toHaveBeenCalledWith('user.name: "the value"');
|
||||
expect(mockSuccessToast).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 type { CellAction, CellActionFactory } from '@kbn/cell-actions';
|
||||
import { isInSecurityApp } from '../../utils';
|
||||
import type { StartServices } from '../../../types';
|
||||
import { createCopyToClipboardCellActionFactory } from '../cell_action/copy_to_clipboard';
|
||||
|
||||
export const createCopyToClipboardDiscoverCellActionFactory = ({
|
||||
services,
|
||||
}: {
|
||||
services: StartServices;
|
||||
}): CellActionFactory<CellAction> => {
|
||||
const { application } = services;
|
||||
|
||||
let currentAppId: string | undefined;
|
||||
application.currentAppId$.subscribe((appId) => {
|
||||
currentAppId = appId;
|
||||
});
|
||||
|
||||
const genericCopyToClipboardActionFactory = createCopyToClipboardCellActionFactory({
|
||||
services,
|
||||
});
|
||||
|
||||
return genericCopyToClipboardActionFactory.combine<CellAction>({
|
||||
isCompatible: async () => isInSecurityApp(currentAppId),
|
||||
});
|
||||
};
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export { createCopyToClipboardLensAction } from './lens/copy_to_clipboard';
|
||||
export { createCopyToClipboardCellActionFactory } from './cell_action/copy_to_clipboard';
|
||||
export { createCopyToClipboardDiscoverCellActionFactory } from './discover/copy_to_clipboard';
|
||||
|
|
|
@ -20,7 +20,8 @@ import { TableId } from '@kbn/securitysolution-data-table';
|
|||
import { TimelineId } from '../../../../common/types';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockFilterManager = services.data.query.filterManager;
|
||||
const mockGlobalFilterManager = services.data.query.filterManager;
|
||||
const mockTimelineFilterManager = createFilterManagerMock();
|
||||
|
||||
const mockState = {
|
||||
...mockGlobalState,
|
||||
|
@ -30,7 +31,7 @@ const mockState = {
|
|||
...mockGlobalState.timeline.timelineById,
|
||||
[TimelineId.active]: {
|
||||
...mockGlobalState.timeline.timelineById[TimelineId.active],
|
||||
filterManager: createFilterManagerMock(),
|
||||
filterManager: mockTimelineFilterManager,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -88,56 +89,64 @@ describe('createFilterInCellActionFactory', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('generic scope execution', () => {
|
||||
const dataTableContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TableId.alertsOnAlertsPage },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
describe('execute', () => {
|
||||
describe('generic scope execution', () => {
|
||||
const dataTableContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TableId.alertsOnAlertsPage },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
|
||||
it('should execute using generic filterManager', async () => {
|
||||
await filterInAction.execute(dataTableContext);
|
||||
expect(mockFilterManager.addFilters).toHaveBeenCalled();
|
||||
expect(
|
||||
mockState.timeline.timelineById[TimelineId.active].filterManager?.addFilters
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline scope execution', () => {
|
||||
const timelineContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TimelineId.active },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
|
||||
it('should execute using timeline filterManager', async () => {
|
||||
await filterInAction.execute(timelineContext);
|
||||
expect(
|
||||
mockState.timeline.timelineById[TimelineId.active].filterManager?.addFilters
|
||||
).toHaveBeenCalled();
|
||||
expect(mockFilterManager.addFilters).not.toHaveBeenCalled();
|
||||
it('should execute using generic filterManager', async () => {
|
||||
await filterInAction.execute(dataTableContext);
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalled();
|
||||
expect(mockTimelineFilterManager.addFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should execute correctly when negateFilters is provided', () => {
|
||||
it('if negateFilters is false, negate should be false (do not exclude)', async () => {
|
||||
describe('timeline scope execution', () => {
|
||||
const timelineContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TimelineId.active },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
|
||||
it('should execute using timeline filterManager', async () => {
|
||||
await filterInAction.execute(timelineContext);
|
||||
expect(mockTimelineFilterManager.addFilters).toHaveBeenCalled();
|
||||
expect(mockGlobalFilterManager.addFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('negateFilters', () => {
|
||||
it('if negateFilters is false, negate should be false (include)', async () => {
|
||||
await filterInAction.execute({
|
||||
...context,
|
||||
metadata: {
|
||||
negateFilters: false,
|
||||
},
|
||||
});
|
||||
expect(mockFilterManager.addFilters).toHaveBeenCalled();
|
||||
// expect(mockCreateFilter).toBeCalledWith(context.field.name, context.field.value, false);
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
meta: expect.objectContaining({
|
||||
negate: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('if negateFilters is true, negate should be true (exclude)', async () => {
|
||||
it('if negateFilters is true, negate should be true (do not include)', async () => {
|
||||
await filterInAction.execute({
|
||||
...context,
|
||||
metadata: {
|
||||
negateFilters: true,
|
||||
},
|
||||
});
|
||||
expect(mockFilterManager.addFilters).toHaveBeenCalled();
|
||||
// expect(mockCreateFilter).toBeCalledWith(context.field.name, context.field.value, true);
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
meta: expect.objectContaining({
|
||||
negate: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,8 @@ import { TimelineId } from '../../../../common/types';
|
|||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockFilterManager = services.data.query.filterManager;
|
||||
const mockGlobalFilterManager = services.data.query.filterManager;
|
||||
const mockTimelineFilterManager = createFilterManagerMock();
|
||||
|
||||
const mockState = {
|
||||
...mockGlobalState,
|
||||
|
@ -30,7 +31,7 @@ const mockState = {
|
|||
...mockGlobalState.timeline.timelineById,
|
||||
[TimelineId.active]: {
|
||||
...mockGlobalState.timeline.timelineById[TimelineId.active],
|
||||
filterManager: createFilterManagerMock(),
|
||||
filterManager: mockTimelineFilterManager,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -82,33 +83,65 @@ describe('createFilterOutCellActionFactory', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('generic scope execution', () => {
|
||||
const dataTableContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TableId.alertsOnAlertsPage },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
describe('execute', () => {
|
||||
describe('generic scope execution', () => {
|
||||
const dataTableContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TableId.alertsOnAlertsPage },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
|
||||
it('should execute using generic filterManager', async () => {
|
||||
await filterOutAction.execute(dataTableContext);
|
||||
expect(mockFilterManager.addFilters).toHaveBeenCalled();
|
||||
expect(
|
||||
mockState.timeline.timelineById[TimelineId.active].filterManager?.addFilters
|
||||
).not.toHaveBeenCalled();
|
||||
it('should execute using generic filterManager', async () => {
|
||||
await filterOutAction.execute(dataTableContext);
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalled();
|
||||
expect(mockTimelineFilterManager.addFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline scope execution', () => {
|
||||
const timelineContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TimelineId.active },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
describe('timeline scope execution', () => {
|
||||
const timelineContext = {
|
||||
...context,
|
||||
metadata: { scopeId: TimelineId.active },
|
||||
} as SecurityCellActionExecutionContext;
|
||||
|
||||
it('should execute using timeline filterManager', async () => {
|
||||
await filterOutAction.execute(timelineContext);
|
||||
expect(
|
||||
mockState.timeline.timelineById[TimelineId.active].filterManager?.addFilters
|
||||
).toHaveBeenCalled();
|
||||
expect(mockFilterManager.addFilters).not.toHaveBeenCalled();
|
||||
it('should execute using timeline filterManager', async () => {
|
||||
await filterOutAction.execute(timelineContext);
|
||||
expect(mockTimelineFilterManager.addFilters).toHaveBeenCalled();
|
||||
expect(mockGlobalFilterManager.addFilters).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('negateFilters', () => {
|
||||
it('if negateFilters is false, negate should be true (exclude)', async () => {
|
||||
await filterOutAction.execute({
|
||||
...context,
|
||||
metadata: {
|
||||
negateFilters: false,
|
||||
},
|
||||
});
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
meta: expect.objectContaining({
|
||||
negate: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('if negateFilters is true, negate should be false (do not exclude)', async () => {
|
||||
await filterOutAction.execute({
|
||||
...context,
|
||||
metadata: {
|
||||
negateFilters: true,
|
||||
},
|
||||
});
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
meta: expect.objectContaining({
|
||||
negate: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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 {
|
||||
createSecuritySolutionStorageMock,
|
||||
kibanaObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
} from '../../../common/mock';
|
||||
import { createStore } from '../../../common/store';
|
||||
import { createFilterInDiscoverCellActionFactory } from './filter_in';
|
||||
import type { SecurityCellActionExecutionContext } from '../../types';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { APP_UI_ID } from '../../../../common';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockGlobalFilterManager = services.data.query.filterManager;
|
||||
|
||||
const currentAppIdSubject$ = new BehaviorSubject<string>(APP_UI_ID);
|
||||
services.application.currentAppId$ = currentAppIdSubject$.asObservable();
|
||||
|
||||
jest.mock('@kbn/ui-actions-plugin/public', () => ({
|
||||
...jest.requireActual('@kbn/ui-actions-plugin/public'),
|
||||
addFilterIn: () => {},
|
||||
addFilterOut: () => {},
|
||||
}));
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const mockStore = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
describe('createFilterInDiscoverCellActionFactory', () => {
|
||||
const createFilterInCellAction = createFilterInDiscoverCellActionFactory({
|
||||
store: mockStore,
|
||||
services,
|
||||
});
|
||||
const filterInAction = createFilterInCellAction({ id: 'testAction' });
|
||||
|
||||
beforeEach(() => {
|
||||
currentAppIdSubject$.next(APP_UI_ID);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const context = {
|
||||
data: [{ field: { name: 'user.name', type: 'text' }, value: 'the value' }],
|
||||
} as SecurityCellActionExecutionContext;
|
||||
|
||||
it('should return display name', () => {
|
||||
expect(filterInAction.getDisplayName(context)).toEqual('Filter In');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(filterInAction.getIconType(context)).toEqual('plusInCircle');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return true if everything is okay', async () => {
|
||||
expect(await filterInAction.isCompatible(context)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if not in security', async () => {
|
||||
currentAppIdSubject$.next('not-security');
|
||||
expect(await filterInAction.isCompatible(context)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if field not allowed', async () => {
|
||||
expect(
|
||||
await filterInAction.isCompatible({
|
||||
...context,
|
||||
data: [
|
||||
{ ...context.data[0], field: { ...context.data[0].field, name: 'signal.reason' } },
|
||||
],
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should execute using generic filterManager', async () => {
|
||||
await filterInAction.execute(context);
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 type { CellAction, CellActionFactory } from '@kbn/cell-actions';
|
||||
import type { SecurityAppStore } from '../../../common/store';
|
||||
import { isInSecurityApp } from '../../utils';
|
||||
import type { StartServices } from '../../../types';
|
||||
import { createFilterInCellActionFactory } from '../cell_action/filter_in';
|
||||
|
||||
export const createFilterInDiscoverCellActionFactory = ({
|
||||
store,
|
||||
services,
|
||||
}: {
|
||||
store: SecurityAppStore;
|
||||
services: StartServices;
|
||||
}): CellActionFactory<CellAction> => {
|
||||
const { application } = services;
|
||||
|
||||
let currentAppId: string | undefined;
|
||||
application.currentAppId$.subscribe((appId) => {
|
||||
currentAppId = appId;
|
||||
});
|
||||
|
||||
const securityFilterInActionFactory = createFilterInCellActionFactory({ store, services });
|
||||
|
||||
return securityFilterInActionFactory.combine<CellAction>({
|
||||
isCompatible: async () => isInSecurityApp(currentAppId),
|
||||
});
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 {
|
||||
createSecuritySolutionStorageMock,
|
||||
kibanaObservable,
|
||||
mockGlobalState,
|
||||
SUB_PLUGINS_REDUCER,
|
||||
} from '../../../common/mock';
|
||||
import { createStore } from '../../../common/store';
|
||||
import { createFilterOutDiscoverCellActionFactory } from './filter_out';
|
||||
import type { SecurityCellActionExecutionContext } from '../../types';
|
||||
import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { APP_UI_ID } from '../../../../common';
|
||||
|
||||
const services = createStartServicesMock();
|
||||
const mockGlobalFilterManager = services.data.query.filterManager;
|
||||
|
||||
const currentAppIdSubject$ = new BehaviorSubject<string>(APP_UI_ID);
|
||||
services.application.currentAppId$ = currentAppIdSubject$.asObservable();
|
||||
|
||||
jest.mock('@kbn/ui-actions-plugin/public', () => ({
|
||||
...jest.requireActual('@kbn/ui-actions-plugin/public'),
|
||||
addFilterIn: () => {},
|
||||
addFilterOut: () => {},
|
||||
}));
|
||||
|
||||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const mockStore = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
|
||||
|
||||
describe('createFilterOutDiscoverCellActionFactory', () => {
|
||||
const createFilterOutCellAction = createFilterOutDiscoverCellActionFactory({
|
||||
store: mockStore,
|
||||
services,
|
||||
});
|
||||
const filterOutAction = createFilterOutCellAction({ id: 'testAction' });
|
||||
|
||||
beforeEach(() => {
|
||||
currentAppIdSubject$.next(APP_UI_ID);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const context = {
|
||||
data: [
|
||||
{
|
||||
field: { name: 'user.name', type: 'text' },
|
||||
value: 'the value',
|
||||
},
|
||||
],
|
||||
} as SecurityCellActionExecutionContext;
|
||||
|
||||
it('should return display name', () => {
|
||||
expect(filterOutAction.getDisplayName(context)).toEqual('Filter Out');
|
||||
});
|
||||
|
||||
it('should return icon type', () => {
|
||||
expect(filterOutAction.getIconType(context)).toEqual('minusInCircle');
|
||||
});
|
||||
|
||||
describe('isCompatible', () => {
|
||||
it('should return true if everything is okay', async () => {
|
||||
expect(await filterOutAction.isCompatible(context)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if not in security', async () => {
|
||||
currentAppIdSubject$.next('not-security');
|
||||
expect(await filterOutAction.isCompatible(context)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false if field not allowed', async () => {
|
||||
expect(
|
||||
await filterOutAction.isCompatible({
|
||||
...context,
|
||||
data: [
|
||||
{ ...context.data[0], field: { ...context.data[0].field, name: 'signal.reason' } },
|
||||
],
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
it('should execute using generic filterManager', async () => {
|
||||
await filterOutAction.execute(context);
|
||||
expect(mockGlobalFilterManager.addFilters).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 type { CellActionFactory, CellAction } from '@kbn/cell-actions';
|
||||
import { isInSecurityApp } from '../../utils';
|
||||
import type { SecurityAppStore } from '../../../common/store';
|
||||
import type { StartServices } from '../../../types';
|
||||
import { createFilterOutCellActionFactory } from '../cell_action/filter_out';
|
||||
|
||||
export const createFilterOutDiscoverCellActionFactory = ({
|
||||
store,
|
||||
services,
|
||||
}: {
|
||||
store: SecurityAppStore;
|
||||
services: StartServices;
|
||||
}): CellActionFactory<CellAction> => {
|
||||
const { application } = services;
|
||||
|
||||
let currentAppId: string | undefined;
|
||||
application.currentAppId$.subscribe((appId) => {
|
||||
currentAppId = appId;
|
||||
});
|
||||
|
||||
const genericFilterOutActionFactory = createFilterOutCellActionFactory({ store, services });
|
||||
|
||||
return genericFilterOutActionFactory.combine<CellAction>({
|
||||
isCompatible: async () => isInSecurityApp(currentAppId),
|
||||
});
|
||||
};
|
|
@ -7,3 +7,5 @@
|
|||
|
||||
export { createFilterInCellActionFactory } from './cell_action/filter_in';
|
||||
export { createFilterOutCellActionFactory } from './cell_action/filter_out';
|
||||
export { createFilterInDiscoverCellActionFactory } from './discover/filter_in';
|
||||
export { createFilterOutDiscoverCellActionFactory } from './discover/filter_out';
|
||||
|
|
|
@ -6,35 +6,49 @@
|
|||
*/
|
||||
|
||||
import { CELL_VALUE_TRIGGER } from '@kbn/embeddable-plugin/public';
|
||||
import type * as H from 'history';
|
||||
import type { History } from 'history';
|
||||
import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID } from '@kbn/discover-plugin/public';
|
||||
import type { SecurityAppStore } from '../common/store/types';
|
||||
import type { StartServices } from '../types';
|
||||
import { createFilterInCellActionFactory, createFilterOutCellActionFactory } from './filter';
|
||||
import {
|
||||
createFilterInCellActionFactory,
|
||||
createFilterInDiscoverCellActionFactory,
|
||||
createFilterOutCellActionFactory,
|
||||
createFilterOutDiscoverCellActionFactory,
|
||||
} from './filter';
|
||||
import {
|
||||
createAddToTimelineLensAction,
|
||||
createAddToTimelineCellActionFactory,
|
||||
createInvestigateInNewTimelineCellActionFactory,
|
||||
createAddToTimelineDiscoverCellActionFactory,
|
||||
} from './add_to_timeline';
|
||||
import { createShowTopNCellActionFactory } from './show_top_n';
|
||||
import {
|
||||
createCopyToClipboardLensAction,
|
||||
createCopyToClipboardCellActionFactory,
|
||||
createCopyToClipboardDiscoverCellActionFactory,
|
||||
} from './copy_to_clipboard';
|
||||
import { createToggleColumnCellActionFactory } from './toggle_column';
|
||||
import { SecurityCellActionsTrigger } from './constants';
|
||||
import type { SecurityCellActionName, SecurityCellActions } from './types';
|
||||
import type {
|
||||
DiscoverCellActionName,
|
||||
DiscoverCellActions,
|
||||
SecurityCellActionName,
|
||||
SecurityCellActions,
|
||||
} from './types';
|
||||
import { enhanceActionWithTelemetry } from './telemetry';
|
||||
|
||||
export const registerUIActions = (
|
||||
store: SecurityAppStore,
|
||||
history: H.History,
|
||||
history: History,
|
||||
services: StartServices
|
||||
) => {
|
||||
registerLensActions(store, services);
|
||||
registerLensEmbeddableActions(store, services);
|
||||
registerDiscoverCellActions(store, services);
|
||||
registerCellActions(store, history, services);
|
||||
};
|
||||
|
||||
const registerLensActions = (store: SecurityAppStore, services: StartServices) => {
|
||||
const registerLensEmbeddableActions = (store: SecurityAppStore, services: StartServices) => {
|
||||
const { uiActions } = services;
|
||||
|
||||
const addToTimelineAction = createAddToTimelineLensAction({ store, order: 1 });
|
||||
|
@ -44,12 +58,47 @@ const registerLensActions = (store: SecurityAppStore, services: StartServices) =
|
|||
uiActions.addTriggerAction(CELL_VALUE_TRIGGER, copyToClipboardAction);
|
||||
};
|
||||
|
||||
const registerDiscoverCellActions = (store: SecurityAppStore, services: StartServices) => {
|
||||
const { uiActions } = services;
|
||||
|
||||
const DiscoverCellActionsFactories: DiscoverCellActions = {
|
||||
filterIn: createFilterInDiscoverCellActionFactory({ store, services }),
|
||||
filterOut: createFilterOutDiscoverCellActionFactory({ store, services }),
|
||||
addToTimeline: createAddToTimelineDiscoverCellActionFactory({ store, services }),
|
||||
copyToClipboard: createCopyToClipboardDiscoverCellActionFactory({ services }),
|
||||
};
|
||||
|
||||
const addDiscoverEmbeddableCellActions = (
|
||||
triggerId: string,
|
||||
actionsOrder: DiscoverCellActionName[]
|
||||
) => {
|
||||
actionsOrder.forEach((actionName, order) => {
|
||||
const actionFactory = DiscoverCellActionsFactories[actionName];
|
||||
if (actionFactory) {
|
||||
const action = actionFactory({ id: `${triggerId}-${actionName}`, order });
|
||||
const actionWithTelemetry = enhanceActionWithTelemetry(action, services);
|
||||
uiActions.addTriggerAction(triggerId, actionWithTelemetry);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// this trigger is already registered by discover search embeddable
|
||||
addDiscoverEmbeddableCellActions(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER_ID, [
|
||||
'filterIn',
|
||||
'filterOut',
|
||||
'addToTimeline',
|
||||
'copyToClipboard',
|
||||
]);
|
||||
};
|
||||
|
||||
const registerCellActions = (
|
||||
store: SecurityAppStore,
|
||||
history: H.History,
|
||||
history: History,
|
||||
services: StartServices
|
||||
) => {
|
||||
const cellActions: SecurityCellActions = {
|
||||
const { uiActions } = services;
|
||||
|
||||
const cellActionsFactories: SecurityCellActions = {
|
||||
filterIn: createFilterInCellActionFactory({ store, services }),
|
||||
filterOut: createFilterOutCellActionFactory({ store, services }),
|
||||
addToTimeline: createAddToTimelineCellActionFactory({ store, services }),
|
||||
|
@ -59,55 +108,37 @@ const registerCellActions = (
|
|||
toggleColumn: createToggleColumnCellActionFactory({ store }),
|
||||
};
|
||||
|
||||
registerCellActionsTrigger({
|
||||
triggerId: SecurityCellActionsTrigger.DEFAULT,
|
||||
cellActions,
|
||||
actionsOrder: ['filterIn', 'filterOut', 'addToTimeline', 'showTopN', 'copyToClipboard'],
|
||||
services,
|
||||
});
|
||||
const registerCellActionsTrigger = (
|
||||
triggerId: SecurityCellActionsTrigger,
|
||||
actionsOrder: SecurityCellActionName[]
|
||||
) => {
|
||||
uiActions.registerTrigger({ id: triggerId });
|
||||
actionsOrder.forEach((actionName, order) => {
|
||||
const actionFactory = cellActionsFactories[actionName];
|
||||
if (actionFactory) {
|
||||
const action = actionFactory({ id: `${triggerId}-${actionName}`, order });
|
||||
const actionWithTelemetry = enhanceActionWithTelemetry(action, services);
|
||||
uiActions.addTriggerAction(triggerId, actionWithTelemetry);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
registerCellActionsTrigger({
|
||||
triggerId: SecurityCellActionsTrigger.DETAILS_FLYOUT,
|
||||
cellActions,
|
||||
actionsOrder: [
|
||||
'filterIn',
|
||||
'filterOut',
|
||||
'addToTimeline',
|
||||
'toggleColumn',
|
||||
'showTopN',
|
||||
'copyToClipboard',
|
||||
],
|
||||
services,
|
||||
});
|
||||
registerCellActionsTrigger(SecurityCellActionsTrigger.DEFAULT, [
|
||||
'filterIn',
|
||||
'filterOut',
|
||||
'addToTimeline',
|
||||
'showTopN',
|
||||
'copyToClipboard',
|
||||
]);
|
||||
|
||||
registerCellActionsTrigger({
|
||||
triggerId: SecurityCellActionsTrigger.ALERTS_COUNT,
|
||||
cellActions,
|
||||
actionsOrder: ['investigateInNewTimeline'],
|
||||
services,
|
||||
});
|
||||
};
|
||||
|
||||
const registerCellActionsTrigger = ({
|
||||
triggerId,
|
||||
cellActions,
|
||||
actionsOrder,
|
||||
services,
|
||||
}: {
|
||||
triggerId: SecurityCellActionsTrigger;
|
||||
cellActions: SecurityCellActions;
|
||||
actionsOrder: SecurityCellActionName[];
|
||||
services: StartServices;
|
||||
}) => {
|
||||
const { uiActions } = services;
|
||||
uiActions.registerTrigger({ id: triggerId });
|
||||
|
||||
actionsOrder.forEach((actionName, order) => {
|
||||
const actionFactory = cellActions[actionName];
|
||||
if (actionFactory) {
|
||||
const action = actionFactory({ id: `${triggerId}-${actionName}`, order });
|
||||
|
||||
uiActions.addTriggerAction(triggerId, enhanceActionWithTelemetry(action, services));
|
||||
}
|
||||
});
|
||||
registerCellActionsTrigger(SecurityCellActionsTrigger.DETAILS_FLYOUT, [
|
||||
'filterIn',
|
||||
'filterOut',
|
||||
'addToTimeline',
|
||||
'toggleColumn',
|
||||
'showTopN',
|
||||
'copyToClipboard',
|
||||
]);
|
||||
|
||||
registerCellActionsTrigger(SecurityCellActionsTrigger.ALERTS_COUNT, ['investigateInNewTimeline']);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import ReactDOM, { unmountComponentAtNode } from 'react-dom';
|
||||
import type * as H from 'history';
|
||||
import type { History } from 'history';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -38,7 +38,7 @@ export const createShowTopNCellActionFactory = createCellActionFactory(
|
|||
services,
|
||||
}: {
|
||||
store: SecurityAppStore;
|
||||
history: H.History;
|
||||
history: History;
|
||||
services: StartServices;
|
||||
}): CellActionTemplate<SecurityCellAction> => ({
|
||||
type: SecurityCellActionType.SHOW_TOP_N,
|
||||
|
|
|
@ -54,14 +54,24 @@ export interface SecurityCellActionExecutionContext extends CellActionExecutionC
|
|||
export type SecurityCellAction = CellAction<SecurityCellActionExecutionContext>;
|
||||
|
||||
export interface SecurityCellActions {
|
||||
filterIn?: CellActionFactory;
|
||||
filterOut?: CellActionFactory;
|
||||
addToTimeline?: CellActionFactory;
|
||||
investigateInNewTimeline?: CellActionFactory;
|
||||
showTopN?: CellActionFactory;
|
||||
copyToClipboard?: CellActionFactory;
|
||||
toggleColumn?: CellActionFactory;
|
||||
filterIn: CellActionFactory;
|
||||
filterOut: CellActionFactory;
|
||||
addToTimeline: CellActionFactory;
|
||||
investigateInNewTimeline: CellActionFactory;
|
||||
showTopN: CellActionFactory;
|
||||
copyToClipboard: CellActionFactory;
|
||||
toggleColumn: CellActionFactory;
|
||||
}
|
||||
|
||||
// All security cell actions names
|
||||
export type SecurityCellActionName = keyof SecurityCellActions;
|
||||
|
||||
export interface DiscoverCellActions {
|
||||
filterIn: CellActionFactory;
|
||||
filterOut: CellActionFactory;
|
||||
addToTimeline: CellActionFactory;
|
||||
copyToClipboard: CellActionFactory;
|
||||
}
|
||||
|
||||
// All Discover search embeddable cell actions names
|
||||
export type DiscoverCellActionName = keyof DiscoverCellActions;
|
||||
|
|
|
@ -61,40 +61,41 @@ export const getUseCellActionsHook = (tableId: TableId) => {
|
|||
useShallowEqualSelector((state) => (getTable(state, tableId) ?? tableDefaults).viewMode) ??
|
||||
tableDefaults.viewMode;
|
||||
|
||||
const cellActionProps = useMemo<UseDataGridColumnsSecurityCellActionsProps>(() => {
|
||||
const cellActionsData =
|
||||
viewMode === VIEW_SELECTION.eventRenderedView
|
||||
? []
|
||||
: columns.map((col) => {
|
||||
const fieldMeta: Partial<BrowserField> | undefined = browserFieldsByName[col.id];
|
||||
return {
|
||||
// TODO use FieldSpec object instead of browserField
|
||||
field: {
|
||||
name: col.id,
|
||||
type: fieldMeta?.type ?? 'keyword',
|
||||
esTypes: fieldMeta?.esTypes ?? [],
|
||||
aggregatable: fieldMeta?.aggregatable ?? false,
|
||||
searchable: fieldMeta?.searchable ?? false,
|
||||
subType: fieldMeta?.subType,
|
||||
},
|
||||
values: (finalData as TimelineNonEcsData[][]).map(
|
||||
(row) => row.find((rowData) => rowData.field === col.id)?.value ?? []
|
||||
),
|
||||
};
|
||||
});
|
||||
const cellActionsMetadata = useMemo(() => ({ scopeId: tableId }), []);
|
||||
|
||||
return {
|
||||
triggerId: SecurityCellActionsTrigger.DEFAULT,
|
||||
data: cellActionsData,
|
||||
metadata: {
|
||||
// cell actions scope
|
||||
scopeId: tableId,
|
||||
},
|
||||
dataGridRef,
|
||||
};
|
||||
}, [viewMode, browserFieldsByName, columns, finalData, dataGridRef]);
|
||||
const cellActionsFields = useMemo<UseDataGridColumnsSecurityCellActionsProps['fields']>(() => {
|
||||
if (viewMode === VIEW_SELECTION.eventRenderedView) {
|
||||
return undefined;
|
||||
}
|
||||
return columns.map((column) => {
|
||||
// TODO use FieldSpec object instead of browserField
|
||||
const browserField: Partial<BrowserField> | undefined = browserFieldsByName[column.id];
|
||||
return {
|
||||
name: column.id,
|
||||
type: browserField?.type ?? 'keyword',
|
||||
esTypes: browserField?.esTypes ?? [],
|
||||
aggregatable: browserField?.aggregatable ?? false,
|
||||
searchable: browserField?.searchable ?? false,
|
||||
subType: browserField?.subType,
|
||||
};
|
||||
});
|
||||
}, [browserFieldsByName, columns, viewMode]);
|
||||
|
||||
const cellActions = useDataGridColumnsSecurityCellActions(cellActionProps);
|
||||
const getCellValue = useCallback<UseDataGridColumnsSecurityCellActionsProps['getCellValue']>(
|
||||
(fieldName, rowIndex) => {
|
||||
const pageRowIndex = rowIndex % finalData.length;
|
||||
return finalData[pageRowIndex].find((rowData) => rowData.field === fieldName)?.value ?? [];
|
||||
},
|
||||
[finalData]
|
||||
);
|
||||
|
||||
const cellActions = useDataGridColumnsSecurityCellActions({
|
||||
triggerId: SecurityCellActionsTrigger.DEFAULT,
|
||||
fields: cellActionsFields,
|
||||
getCellValue,
|
||||
metadata: cellActionsMetadata,
|
||||
dataGridRef,
|
||||
});
|
||||
|
||||
const getCellActions = useCallback(
|
||||
(_columnId: string, columnIndex: number) => {
|
||||
|
|
|
@ -159,6 +159,7 @@
|
|||
"@kbn/ecs",
|
||||
"@kbn/url-state",
|
||||
"@kbn/ml-anomaly-utils",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/dev-proc-runner",
|
||||
"@kbn/cloud-chat-plugin"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue