[Security Solution] Update CellActions field type to be FieldSpec #157243 (#157834)

issue: https://github.com/elastic/kibana/issues/150347

## Context
Some Actions need to access `FieldSpec` data, which is not present on
the `CellActions` API (`subType`and `isMapped`). So we are updating the
`CellActions` `field` property to be compatible with `FieldSpec`.

## Summary

This PR is the first step to fix
https://github.com/elastic/kibana/issues/150347.
* Updates the `CellActions` to support an array of data objects, each
containing field (`FieldSpec`) and value
* Create a new `SecurityCellActions` component that accepts a field name
and loads `FieldSpec` from the Dataview.

## Examples
Before: 
```tsx
 <SecurityCellActions
      value={'admin'}
      field={{
        name: 'user.name',
        type: 'keyword',
        searchable: true,
        aggregatable: true,
        ...
      }} />
```
After:
```tsx
 <SecurityCellActions data={{ field: 'user.name', value: 'admin' }}/>
```
`SecurityCellActions` will load the spec from the Dataview and provide
it to `CellActons`.

`CellActons` now also support an of fields instead of only one field. It
will be useful when rendering cell actions for aggregated data like on
the Entity Analytic page. But for now, the actions are not supporting
multiple values, we need to rewrite them
https://github.com/elastic/kibana/issues/159480.

### Next steps
We must refactor the Security Solution to get `FieldSpec` from the
`DataView` instead of using BrowserFields. Ideally, we have to do that
for every `CellAction` call so the actions can access the `subType`
property.
- [x] ~Refactor the Security Solution CellActions calls to get
`FieldSpec` from the `DataView`~
- [x] Refactor data grid cell actions to get `FieldSpec` from the
`DataView`
- [ ] Rewrite actions to support multiple fields and use them on the
investigation in timeline (.andFilters)
- [ ] Fix https://github.com/elastic/kibana/issues/150347 using
`subType` from `fieldSpec`
- [ ] Fix https://github.com/elastic/kibana/issues/154714 using
`isMapped` from `fieldSpec`

### Extra information
*** Previously we were mixing `esTypes` and `kbnTypes`. For example, if
the `esType` is a keyword the `kbnType` has to be a `string`.

[Here](9799dbba27/packages/kbn-field-types/src/types.ts (L22))
you can check all possible ES and KBN types and
[here](9799dbba27/packages/kbn-field-types/src/kbn_field_types_factory.ts)
you can see the mapping between esType and kbnType


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Pablo Machado 2023-06-22 15:20:53 +02:00 committed by GitHub
parent 3fc1e68146
commit 5fb9709d4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 1008 additions and 590 deletions

View file

@ -8,6 +8,7 @@
import React from 'react';
import { ComponentStory } from '@storybook/react';
import { FieldSpec } from '@kbn/data-views-plugin/common';
import { CellActionsProvider } from '../context/cell_actions_context';
import { makeAction } from '../mocks/helpers';
import { CellActions } from '../components/cell_actions';
@ -16,7 +17,13 @@ import type { CellActionsProps } from '../types';
const TRIGGER_ID = 'testTriggerId';
const FIELD = { name: 'name', value: '123', type: 'text' };
const VALUE = '123';
const FIELD: FieldSpec = {
name: 'name',
type: 'text',
searchable: true,
aggregatable: true,
};
const getCompatibleActions = () =>
Promise.resolve([
@ -62,24 +69,56 @@ DefaultWithControls.args = {
showActionTooltips: true,
mode: CellActionsMode.INLINE,
triggerId: TRIGGER_ID,
field: FIELD,
data: [
{
field: FIELD,
value: '',
},
],
visibleCellActions: 3,
};
export const CellActionInline = ({}: {}) => (
<CellActions mode={CellActionsMode.INLINE} triggerId={TRIGGER_ID} field={FIELD}>
<CellActions
mode={CellActionsMode.INLINE}
triggerId={TRIGGER_ID}
data={[
{
field: FIELD,
value: VALUE,
},
]}
>
Field value
</CellActions>
);
export const CellActionHoverPopoverDown = ({}: {}) => (
<CellActions mode={CellActionsMode.HOVER_DOWN} triggerId={TRIGGER_ID} field={FIELD}>
<CellActions
mode={CellActionsMode.HOVER_DOWN}
triggerId={TRIGGER_ID}
data={[
{
field: FIELD,
value: VALUE,
},
]}
>
Hover me
</CellActions>
);
export const CellActionHoverPopoverRight = ({}: {}) => (
<CellActions mode={CellActionsMode.HOVER_RIGHT} triggerId={TRIGGER_ID} field={FIELD}>
<CellActions
mode={CellActionsMode.HOVER_RIGHT}
triggerId={TRIGGER_ID}
data={[
{
field: FIELD,
value: VALUE,
},
]}
>
Hover me
</CellActions>
);

View file

@ -21,7 +21,12 @@ describe('Default createCopyToClipboardActionFactory', () => {
});
const copyToClipboardAction = copyToClipboardActionFactory({ id: 'testAction' });
const context = {
field: { name: 'user.name', value: 'the value', type: 'text' },
data: [
{
field: { name: 'user.name', type: 'text' },
value: 'the value',
},
],
} as CellActionExecutionContext;
beforeEach(() => {
@ -52,7 +57,12 @@ describe('Default createCopyToClipboardActionFactory', () => {
it('should escape value', async () => {
await copyToClipboardAction.execute({
...context,
field: { ...context.field, value: 'the "value"' },
data: [
{
...context.data[0],
value: 'the "value"',
},
],
});
expect(mockCopy).toHaveBeenCalledWith('user.name: "the \\"value\\""');
expect(mockSuccessToast).toHaveBeenCalled();
@ -61,7 +71,12 @@ describe('Default createCopyToClipboardActionFactory', () => {
it('should suport multiple values', async () => {
await copyToClipboardAction.execute({
...context,
field: { ...context.field, value: ['the "value"', 'another value', 'last value'] },
data: [
{
...context.data[0],
value: ['the "value"', 'another value', 'last value'],
},
],
});
expect(mockCopy).toHaveBeenCalledWith(
'user.name: "the \\"value\\"" AND "another value" AND "last value"'

View file

@ -31,13 +31,23 @@ export const createCopyToClipboardActionFactory = createCellActionFactory(
getIconType: () => ICON,
getDisplayName: () => COPY_TO_CLIPBOARD,
getDisplayNameTooltip: () => COPY_TO_CLIPBOARD,
isCompatible: async ({ field }) => field.name != null,
execute: async ({ field }) => {
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
field.name != null
);
},
execute: async ({ data }) => {
const field = data[0]?.field;
const value = data[0]?.value;
let textValue: undefined | string;
if (field.value != null) {
textValue = Array.isArray(field.value)
? field.value.map((value) => `"${escapeValue(value)}"`).join(' AND ')
: `"${escapeValue(field.value)}"`;
if (value != null) {
textValue = Array.isArray(value)
? value.map((v) => `"${escapeValue(v)}"`).join(' AND ')
: `"${escapeValue(value)}"`;
}
const text = textValue ? `${field.name}: ${textValue}` : field.name;
const isSuccess = copy(text, { debug: true });

View file

@ -26,7 +26,12 @@ describe('createFilterInActionFactory', () => {
});
const filterInAction = filterInActionFactory({ id: 'testAction' });
const context = makeActionContext({
field: { name: fieldName, value, type: 'text' },
data: [
{
field: { name: fieldName, type: 'text', searchable: true, aggregatable: true },
value,
},
],
});
beforeEach(() => {
@ -50,7 +55,11 @@ describe('createFilterInActionFactory', () => {
expect(
await filterInAction.isCompatible({
...context,
field: { ...context.field, name: '' },
data: [
{
field: { ...context.data[0].field, name: '' },
},
],
})
).toEqual(false);
});
@ -74,7 +83,12 @@ describe('createFilterInActionFactory', () => {
it('should create filter query with array value', async () => {
await filterInAction.execute({
...context,
field: { ...context.field, value: [value] },
data: [
{
...context.data[0],
value: [value],
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({
key: fieldName,
@ -86,7 +100,12 @@ describe('createFilterInActionFactory', () => {
it('should create negate filter query with null value', async () => {
await filterInAction.execute({
...context,
field: { ...context.field, value: null },
data: [
{
...context.data[0],
value: null,
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: null, negate: true });
});
@ -94,7 +113,12 @@ describe('createFilterInActionFactory', () => {
it('should create negate filter query with undefined value', async () => {
await filterInAction.execute({
...context,
field: { ...context.field, value: undefined },
data: [
{
...context.data[0],
value: undefined,
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({
key: fieldName,
@ -106,7 +130,12 @@ describe('createFilterInActionFactory', () => {
it('should create negate filter query with empty string value', async () => {
await filterInAction.execute({
...context,
field: { ...context.field, value: '' },
data: [
{
...context.data[0],
value: '',
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: '', negate: true });
});
@ -114,7 +143,12 @@ describe('createFilterInActionFactory', () => {
it('should create negate filter query with empty array value', async () => {
await filterInAction.execute({
...context,
field: { ...context.field, value: [] },
data: [
{
...context.data[0],
value: [],
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: true });
});

View file

@ -22,9 +22,18 @@ export const createFilterInActionFactory = createCellActionFactory(
getIconType: () => ICON,
getDisplayName: () => FILTER_IN,
getDisplayNameTooltip: () => FILTER_IN,
isCompatible: async ({ field }) => !!field.name,
execute: async ({ field }) => {
addFilterIn({ filterManager, fieldName: field.name, value: field.value });
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
!!field.name
);
},
execute: async ({ data }) => {
const field = data[0]?.field;
const value = data[0]?.value;
addFilterIn({ filterManager, fieldName: field.name, value });
},
})
);

View file

@ -24,7 +24,12 @@ describe('createFilterOutAction', () => {
const filterOutActionFactory = createFilterOutActionFactory({ filterManager: mockFilterManager });
const filterOutAction = filterOutActionFactory({ id: 'testAction' });
const context = makeActionContext({
field: { name: fieldName, value, type: 'text' },
data: [
{
field: { name: fieldName, type: 'text', searchable: true, aggregatable: true },
value,
},
],
});
beforeEach(() => {
@ -48,7 +53,11 @@ describe('createFilterOutAction', () => {
expect(
await filterOutAction.isCompatible({
...context,
field: { ...context.field, name: '' },
data: [
{
field: { ...context.data[0].field, name: '' },
},
],
})
).toEqual(false);
});
@ -68,7 +77,12 @@ describe('createFilterOutAction', () => {
it('should create negate filter query with array value', async () => {
await filterOutAction.execute({
...context,
field: { ...context.field, value: [value] },
data: [
{
field: { ...context.data[0].field },
value: [value],
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({
key: fieldName,
@ -80,7 +94,12 @@ describe('createFilterOutAction', () => {
it('should create filter query with null value', async () => {
await filterOutAction.execute({
...context,
field: { ...context.field, value: null },
data: [
{
field: { ...context.data[0].field },
value: null,
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: null, negate: false });
});
@ -88,7 +107,12 @@ describe('createFilterOutAction', () => {
it('should create filter query with undefined value', async () => {
await filterOutAction.execute({
...context,
field: { ...context.field, value: undefined },
data: [
{
field: { ...context.data[0].field },
value: undefined,
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({
key: fieldName,
@ -100,7 +124,12 @@ describe('createFilterOutAction', () => {
it('should create negate filter query with empty string value', async () => {
await filterOutAction.execute({
...context,
field: { ...context.field, value: '' },
data: [
{
field: { ...context.data[0].field },
value: '',
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: '', negate: false });
});
@ -108,7 +137,12 @@ describe('createFilterOutAction', () => {
it('should create negate filter query with empty array value', async () => {
await filterOutAction.execute({
...context,
field: { ...context.field, value: [] },
data: [
{
field: { ...context.data[0].field },
value: [],
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: false });
});

View file

@ -22,12 +22,22 @@ export const createFilterOutActionFactory = createCellActionFactory(
getIconType: () => ICON,
getDisplayName: () => FILTER_OUT,
getDisplayNameTooltip: () => FILTER_OUT,
isCompatible: async ({ field }) => !!field.name,
execute: async ({ field }) => {
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
!!field.name
);
},
execute: async ({ data }) => {
const field = data[0]?.field;
const value = data[0]?.value;
addFilterOut({
filterManager,
fieldName: field.name,
value: field.value,
value,
});
},
})

View file

@ -11,9 +11,20 @@ import React from 'react';
import { CellActions } from './cell_actions';
import { CellActionsMode } from '../constants';
import { CellActionsProvider } from '../context/cell_actions_context';
import type { FieldSpec } from '@kbn/data-views-plugin/common';
const TRIGGER_ID = 'test-trigger-id';
const FIELD = { name: 'name', value: '123', type: 'text' };
const VALUE = '123';
const FIELD: FieldSpec = {
name: 'name',
type: 'text',
searchable: true,
aggregatable: true,
};
const DATA = {
field: FIELD,
value: VALUE,
};
jest.mock('./hover_actions_popover', () => ({
HoverActionsPopover: jest.fn((props) => (
@ -27,7 +38,7 @@ describe('CellActions', () => {
const { queryByTestId } = render(
<CellActionsProvider getTriggerCompatibleActions={getActions}>
<CellActions mode={CellActionsMode.INLINE} triggerId={TRIGGER_ID} field={FIELD}>
<CellActions mode={CellActionsMode.INLINE} triggerId={TRIGGER_ID} data={DATA}>
Field value
</CellActions>
</CellActionsProvider>
@ -46,7 +57,7 @@ describe('CellActions', () => {
const { queryByTestId } = render(
<CellActionsProvider getTriggerCompatibleActions={getActions}>
<CellActions mode={CellActionsMode.INLINE} triggerId={TRIGGER_ID} field={FIELD}>
<CellActions mode={CellActionsMode.INLINE} triggerId={TRIGGER_ID} data={DATA}>
Field value
</CellActions>
</CellActionsProvider>
@ -65,7 +76,7 @@ describe('CellActions', () => {
const { getByTestId } = render(
<CellActionsProvider getTriggerCompatibleActions={getActions}>
<CellActions mode={CellActionsMode.HOVER_DOWN} triggerId={TRIGGER_ID} field={FIELD}>
<CellActions mode={CellActionsMode.HOVER_DOWN} triggerId={TRIGGER_ID} data={DATA}>
Field value
</CellActions>
</CellActionsProvider>
@ -85,7 +96,7 @@ describe('CellActions', () => {
const { getByTestId } = render(
<CellActionsProvider getTriggerCompatibleActions={getActions}>
<CellActions mode={CellActionsMode.HOVER_RIGHT} triggerId={TRIGGER_ID} field={FIELD}>
<CellActions mode={CellActionsMode.HOVER_RIGHT} triggerId={TRIGGER_ID} data={DATA}>
Field value
</CellActions>
</CellActionsProvider>

View file

@ -8,13 +8,14 @@
import React, { useMemo, useRef } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { isArray } from 'lodash/fp';
import { InlineActions } from './inline_actions';
import { HoverActionsPopover } from './hover_actions_popover';
import { CellActionsMode } from '../constants';
import type { CellActionsProps, CellActionExecutionContext } from '../types';
export const CellActions: React.FC<CellActionsProps> = ({
field,
data,
triggerId,
children,
mode,
@ -26,14 +27,16 @@ export const CellActions: React.FC<CellActionsProps> = ({
}) => {
const nodeRef = useRef<HTMLDivElement | null>(null);
const dataArray = useMemo(() => (isArray(data) ? data : [data]), [data]);
const actionContext: CellActionExecutionContext = useMemo(
() => ({
field,
data: dataArray,
trigger: { id: triggerId },
nodeRef,
metadata,
}),
[field, triggerId, metadata]
[dataArray, triggerId, metadata]
);
const anchorPosition = useMemo(
@ -41,7 +44,10 @@ export const CellActions: React.FC<CellActionsProps> = ({
[mode]
);
const dataTestSubj = `cellActions-renderContent-${field.name}`;
const dataTestSubj = `cellActions-renderContent-${dataArray
.map(({ field }) => field.name)
.join('-')}`;
if (mode === CellActionsMode.HOVER_DOWN || mode === CellActionsMode.HOVER_RIGHT) {
return (
<div className={className} ref={nodeRef} data-test-subj={dataTestSubj}>

View file

@ -128,10 +128,15 @@ const ExtraActionsPopOverContent: React.FC<ExtraActionsPopOverContentProps> = ({
)),
[actionContext, actions, closePopOver]
);
return (
<>
<EuiScreenReaderOnly>
<p>{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}</p>
<p>
{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(
actionContext.data.map(({ field }) => field.name).join(', ')
)}
</p>
</EuiScreenReaderOnly>
<EuiContextMenuPanel size="s" items={items} />
</>

View file

@ -19,7 +19,11 @@ const defaultProps = {
visibleCellActions: 4,
actionContext: {
trigger: { id: 'triggerId' },
field: { name: 'fieldName' },
data: [
{
field: { name: 'fieldName' },
},
],
} as CellActionExecutionContext,
showActionTooltips: false,
};

View file

@ -141,7 +141,11 @@ export const HoverActionsPopover: React.FC<Props> = ({
{showHoverContent && (
<div css={hoverContentWrapperCSS}>
<EuiScreenReaderOnly>
<p>{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(actionContext.field.name)}</p>
<p>
{YOU_ARE_IN_A_DIALOG_CONTAINING_OPTIONS(
actionContext.data.map(({ field }) => field.name).join(', ')
)}
</p>
</EuiScreenReaderOnly>
{visibleActions.map((action) => (
<ActionItem

View file

@ -31,14 +31,30 @@ 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 field1 = { name: 'column1', values: ['0.0', '0.1', '0.2', '0.3'], type: 'text' };
const field2 = { name: 'column2', values: ['1.0', '1.1', '1.2', '1.3'], type: 'keyword' };
const values2 = ['1.0', '1.1', '1.2', '1.3'];
const field2 = {
name: 'column2',
type: 'string',
searchable: true,
aggregatable: true,
};
const columns = [{ id: field1.name }, { id: field2.name }];
const mockCloseCellPopover = jest.fn();
const useDataGridColumnsCellActionsProps: UseDataGridColumnsCellActionsProps = {
fields: [field1, field2],
data: [
{ field: field1, values: values1 },
{ field: field2, values: values2 },
],
triggerId: 'testTriggerId',
metadata: { some: 'value' },
dataGridRef: {
@ -138,7 +154,17 @@ describe('useDataGridColumnsCellActions', () => {
await waitFor(() => {
expect(action1.execute).toHaveBeenCalledWith(
expect.objectContaining({
field: { name: field1.name, type: field1.type, value: field1.values[1] },
data: [
{
value: values1[1],
field: {
name: field1.name,
type: field1.type,
aggregatable: true,
searchable: true,
},
},
],
trigger: { id: useDataGridColumnsCellActionsProps.triggerId },
})
);
@ -151,7 +177,17 @@ describe('useDataGridColumnsCellActions', () => {
await waitFor(() => {
expect(action2.execute).toHaveBeenCalledWith(
expect.objectContaining({
field: { name: field2.name, type: field2.type, value: field2.values[2] },
data: [
{
value: values2[2],
field: {
name: field2.name,
type: field2.type,
aggregatable: true,
searchable: true,
},
},
],
trigger: { id: useDataGridColumnsCellActionsProps.triggerId },
})
);
@ -171,7 +207,17 @@ describe('useDataGridColumnsCellActions', () => {
await waitFor(() => {
expect(action1.execute).toHaveBeenCalledWith(
expect.objectContaining({
field: { name: field1.name, type: field1.type, value: field1.values[1] },
data: [
{
value: values1[1],
field: {
name: field1.name,
type: field1.type,
aggregatable: true,
searchable: true,
},
},
],
})
);
});
@ -196,7 +242,7 @@ describe('useDataGridColumnsCellActions', () => {
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
initialProps: {
...useDataGridColumnsCellActionsProps,
fields: [],
data: [],
},
});
@ -210,7 +256,7 @@ describe('useDataGridColumnsCellActions', () => {
const { result, waitForNextUpdate } = renderHook(useDataGridColumnsCellActions, {
initialProps: {
...useDataGridColumnsCellActionsProps,
fields: undefined,
data: undefined,
},
});

View file

@ -16,12 +16,12 @@ import type {
CellAction,
CellActionCompatibilityContext,
CellActionExecutionContext,
CellActionField,
CellActionsData,
CellActionsProps,
} from '../types';
import { useBulkLoadActions } from './use_load_actions';
interface BulkField extends Pick<CellActionField, 'name' | 'type'> {
interface BulkData extends Omit<CellActionsData, 'value'> {
/**
* Array containing all the values of the field in the visible page, indexed by rowIndex
*/
@ -30,7 +30,7 @@ interface BulkField extends Pick<CellActionField, 'name' | 'type'> {
export interface UseDataGridColumnsCellActionsProps
extends Pick<CellActionsProps, 'triggerId' | 'metadata' | 'disabledActionTypes'> {
fields?: BulkField[];
data?: BulkData[];
dataGridRef: MutableRefObject<EuiDataGridRefProps | null>;
}
export type UseDataGridColumnsCellActions<
@ -38,7 +38,7 @@ export type UseDataGridColumnsCellActions<
> = (props: P) => EuiDataGridColumnCellAction[][];
export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({
fields,
data,
triggerId,
metadata,
dataGridRef,
@ -46,12 +46,12 @@ export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({
}) => {
const bulkContexts: CellActionCompatibilityContext[] = useMemo(
() =>
fields?.map(({ values, ...field }) => ({
field, // we are getting the actions for the whole column field, so the compatibility check will be done without the value
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,
})) ?? [],
[fields, triggerId, metadata]
[triggerId, metadata, data]
);
const { loading, value: columnsActions } = useBulkLoadActions(bulkContexts, {
@ -61,37 +61,44 @@ export const useDataGridColumnsCellActions: UseDataGridColumnsCellActions = ({
const columnsCellActions = useMemo<EuiDataGridColumnCellAction[][]>(() => {
if (loading) {
return (
fields?.map(() => [
data?.map(() => [
() => <EuiLoadingSpinner size="s" data-test-subj="dataGridColumnCellAction-loading" />,
]) ?? []
);
}
if (!columnsActions || !fields || fields.length === 0) {
if (!columnsActions || !data || data.length === 0) {
return [];
}
// 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 [];
}
return columnsActions.map((actions, columnIndex) =>
actions.map((action) =>
createColumnCellAction({
action,
metadata,
triggerId,
field: fields[columnIndex],
data: data[columnIndex],
dataGridRef,
})
)
);
}, [columnsActions, fields, loading, metadata, triggerId, dataGridRef]);
}, [loading, columnsActions, data, metadata, triggerId, dataGridRef]);
return columnsCellActions;
};
interface CreateColumnCellActionParams
extends Pick<UseDataGridColumnsCellActionsProps, 'triggerId' | 'metadata' | 'dataGridRef'> {
field: BulkField;
data: BulkData;
action: CellAction;
}
const createColumnCellAction = ({
field,
data: { field, values },
action,
metadata,
triggerId,
@ -102,11 +109,15 @@ const createColumnCellAction = ({
const buttonRef = useRef<HTMLAnchorElement | null>(null);
const actionContext: CellActionExecutionContext = useMemo(() => {
const { name, type, values } = field;
// 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];
return {
field: { name, type, value },
data: [
{
field,
value,
},
],
trigger: { id: triggerId },
nodeRef,
metadata,

View file

@ -27,11 +27,17 @@ export const makeActionContext = (
override: Partial<CellActionExecutionContext> = {}
): CellActionExecutionContext => ({
trigger: { id: 'triggerId' },
field: {
name: 'fieldName',
type: 'keyword',
value: 'some value',
},
data: [
{
field: {
name: 'fieldName',
type: 'keyword',
searchable: true,
aggregatable: true,
},
value: 'some value',
},
],
nodeRef: {} as MutableRefObject<HTMLElement>,
metadata: undefined,
...override,

View file

@ -10,6 +10,7 @@ import type {
ActionExecutionContext,
UiActionsService,
} from '@kbn/ui-actions-plugin/public';
import type { FieldSpec } from '@kbn/data-views-plugin/common';
import type { CellActionsMode } from './constants';
export interface CellActionsProviderProps {
@ -20,39 +21,25 @@ export interface CellActionsProviderProps {
getTriggerCompatibleActions: UiActionsService['getTriggerCompatibleActions'];
}
export interface CellActionField {
/**
* Field name.
* Example: 'host.name'
*/
name: string;
/**
* Field type.
* Example: 'keyword'
*/
type: string;
/**
* Field value.
* Example: 'My-Laptop'
*/
value: string | string[] | null | undefined;
/**
* When true the field supports aggregations.
*
* It defaults to false.
*
* You can verify if a field is aggregatable on kibana/management/kibana/dataViews.
*/
aggregatable?: boolean;
}
type Metadata = Record<string, unknown>;
export interface CellActionsProps {
export type CellActionFieldValue = string | string[] | null | undefined;
export interface CellActionsData {
/**
* The field specification
*/
field: FieldSpec;
/**
* Common set of properties used by most actions.
*/
field: CellActionField;
value: CellActionFieldValue;
}
export interface CellActionsProps {
data: CellActionsData | CellActionsData[];
/**
* The trigger in which the actions are registered.
*/
@ -89,7 +76,8 @@ export interface CellActionsProps {
}
export interface CellActionExecutionContext extends ActionExecutionContext {
field: CellActionField;
data: CellActionsData[];
/**
* Ref to the node where the cell action are rendered.
*/
@ -104,13 +92,15 @@ export interface CellActionExecutionContext extends ActionExecutionContext {
* Subset of `CellActionExecutionContext` used only for the compatibility check in the `isCompatible` function.
* It omits the references and the `field.value`.
*/
export interface CellActionCompatibilityContext<
C extends CellActionExecutionContext = CellActionExecutionContext
> extends ActionExecutionContext {
/**
* The object containing the field name and type, needed for the compatibility check
* CellActionsData containing the field spec but not the value for the compatibility check
*/
field: Omit<C['field'], 'value'>;
data: Array<Omit<C['data'][number], 'value'>>;
/**
* Extra configurations for actions.
*/

View file

@ -19,6 +19,7 @@
"@kbn/data-plugin",
"@kbn/es-query",
"@kbn/ui-actions-plugin",
"@kbn/data-views-plugin",
],
"exclude": ["target/**/*"]
}

View file

@ -36,6 +36,7 @@ export type ColumnHeaderOptions = Pick<
| 'isResizable'
> & {
aggregatable?: boolean;
searchable?: boolean;
category?: string;
columnHeaderType: ColumnHeaderType;
description?: string | null;

View file

@ -171,14 +171,20 @@ describe('DataTable', () => {
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({
triggerId: 'mockCellActionsTrigger',
fields: [
data: [
{
name: '@timestamp',
values: [data[0]?.data[0]?.value],
type: 'date',
aggregatable: true,
field: {
name: '@timestamp',
type: 'date',
aggregatable: true,
esTypes: ['date'],
searchable: true,
subType: undefined,
},
},
],
metadata: {
scopeId: 'table-test',
},
@ -196,7 +202,7 @@ describe('DataTable', () => {
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith(
expect.objectContaining({
fields: [],
data: [],
})
);
});

View file

@ -328,21 +328,27 @@ export const DataTableComponent = React.memo<DataTableProps>(
);
const columnsCellActionsProps = useMemo(() => {
const fields = !cellActionsTriggerId
const columnsCellActionData = !cellActionsTriggerId
? []
: columnHeaders.map((column) => ({
name: column.id,
type: column.type ?? 'keyword',
// TODO use FieldSpec object instead of column
field: {
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
),
aggregatable: column.aggregatable,
}));
return {
triggerId: cellActionsTriggerId || '',
fields,
data: columnsCellActionData,
metadata: {
scopeId: id,
},

View file

@ -23,6 +23,7 @@ export const defaultHeaders: ColumnHeaderOptions[] = [
type: 'date',
esTypes: ['date'],
aggregatable: true,
searchable: true,
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
},
{

View file

@ -25,7 +25,12 @@ const store = {
const value = 'the-value';
const context = {
field: { name: 'user.name', value, type: 'text' },
data: [
{
field: { name: 'user.name', type: 'text' },
value,
},
],
} as CellActionExecutionContext;
const defaultDataProvider = {
@ -74,7 +79,12 @@ describe('createAddToTimelineCellAction', () => {
expect(
await addToTimelineAction.isCompatible({
...context,
field: { ...context.field, name: 'signal.reason' },
data: [
{
...context.data[0],
field: { ...context.data[0].field, name: 'signal.reason' },
},
],
})
).toEqual(false);
});
@ -89,7 +99,7 @@ describe('createAddToTimelineCellAction', () => {
it('should execute with number value', async () => {
await addToTimelineAction.execute({
field: { name: 'process.parent.pid', value: 12345, type: 'number' },
data: [{ field: { name: 'process.parent.pid', type: 'number' }, value: 12345 }],
} as unknown as CellActionExecutionContext); // TODO: remove `as unknown` when number value type is supported
expect(mockDispatch).toHaveBeenCalledWith(
set(
@ -112,8 +122,8 @@ describe('createAddToTimelineCellAction', () => {
it('should execute with null value', async () => {
await addToTimelineAction.execute({
field: { name: 'user.name', value: null, type: 'text' },
} as CellActionExecutionContext);
data: [{ field: { name: 'user.name', type: 'text' }, value: null }],
} as unknown as CellActionExecutionContext);
expect(mockDispatch).toHaveBeenCalledWith(
set(
'payload.providers[0]',
@ -137,8 +147,8 @@ describe('createAddToTimelineCellAction', () => {
const value2 = 'value2';
const value3 = 'value3';
await addToTimelineAction.execute({
field: { name: 'user.name', value: [value, value2, value3], type: 'text' },
} as CellActionExecutionContext);
data: [{ field: { name: 'user.name', type: 'text' }, value: [value, value2, value3] }],
} as unknown as CellActionExecutionContext);
expect(mockDispatch).toHaveBeenCalledWith(
set(
'payload.providers[0]',
@ -166,10 +176,15 @@ describe('createAddToTimelineCellAction', () => {
it('should show warning if no provider added', async () => {
await addToTimelineAction.execute({
...context,
field: {
...context.field,
type: GEO_FIELD_TYPE,
},
data: [
{
...context.data[0],
field: {
...context.data[0].field,
type: GEO_FIELD_TYPE,
},
},
],
});
expect(mockDispatch).not.toHaveBeenCalled();
expect(mockWarningToast).toHaveBeenCalled();

View file

@ -38,9 +38,19 @@ export const createAddToTimelineCellActionFactory = createCellActionFactory(
getIconType: () => ADD_TO_TIMELINE_ICON,
getDisplayName: () => ADD_TO_TIMELINE,
getDisplayNameTooltip: () => ADD_TO_TIMELINE,
isCompatible: async ({ field }) =>
fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type),
execute: async ({ field: { value, type, name }, metadata }) => {
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
fieldHasCellActions(field.name) &&
isValidDataProviderField(field.name, field.type)
);
},
execute: async ({ data, metadata }) => {
const { name, type } = data[0]?.field;
const value = data[0]?.value;
const values = Array.isArray(value) ? value : [value];
const [firstValue, ...andValues] = values;
const [dataProvider] =

View file

@ -25,7 +25,12 @@ const store = {
const value = 'the-value';
const context = {
field: { name: 'user.name', value, type: 'text' },
data: [
{
field: { name: 'user.name', type: 'text' },
value,
},
],
} as CellActionExecutionContext;
const defaultAddProviderAction = {
@ -77,7 +82,12 @@ describe('createAddToNewTimelineCellAction', () => {
expect(
await addToTimelineAction.isCompatible({
...context,
field: { ...context.field, name: 'signal.reason' },
data: [
{
...context.data[0],
field: { ...context.data[0].field, name: 'signal.reason' },
},
],
})
).toEqual(false);
});
@ -93,10 +103,12 @@ describe('createAddToNewTimelineCellAction', () => {
it('should show warning if no provider added', async () => {
await addToTimelineAction.execute({
...context,
field: {
...context.field,
type: GEO_FIELD_TYPE,
},
data: [
{
...context.data[0],
field: { ...context.data[0].field, type: GEO_FIELD_TYPE },
},
],
});
expect(mockDispatch).not.toHaveBeenCalled();
expect(mockWarningToast).toHaveBeenCalled();

View file

@ -38,14 +38,24 @@ export const createInvestigateInNewTimelineCellActionFactory = createCellActionF
getIconType: () => ADD_TO_TIMELINE_ICON,
getDisplayName: () => INVESTIGATE_IN_TIMELINE,
getDisplayNameTooltip: () => INVESTIGATE_IN_TIMELINE,
isCompatible: async ({ field }) =>
fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type),
execute: async ({ field, metadata }) => {
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
fieldHasCellActions(field.name) &&
isValidDataProviderField(field.name, field.type)
);
},
execute: async ({ data, metadata }) => {
const field = data[0]?.field;
const value = data[0]?.value;
const dataProviders =
createDataProviders({
contextId: TimelineId.active,
fieldType: field.type,
values: field.value,
values: value,
field: field.name,
negate: metadata?.negateFilters === true,
}) ?? [];

View file

@ -22,6 +22,13 @@ export const createCopyToClipboardCellActionFactory = ({
});
return genericCopyToClipboardActionFactory.combine<SecurityCellAction>({
type: SecurityCellActionType.COPY,
isCompatible: async ({ field }) => fieldHasCellActions(field.name),
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
fieldHasCellActions(field.name)
);
},
});
};

View file

@ -54,7 +54,12 @@ describe('createFilterInCellActionFactory', () => {
});
const context = {
field: { name: 'user.name', value: 'the value', type: 'text' },
data: [
{
field: { name: 'user.name', type: 'text' },
value: 'the value',
},
],
} as SecurityCellActionExecutionContext;
it('should return display name', () => {
@ -73,7 +78,11 @@ describe('createFilterInCellActionFactory', () => {
expect(
await filterInAction.isCompatible({
...context,
field: { ...context.field, name: 'signal.reason' },
data: [
{
field: { ...context.data[0].field, name: 'signal.reason' },
},
],
})
).toEqual(false);
});

View file

@ -29,8 +29,20 @@ export const createFilterInCellActionFactory = ({
return genericFilterInActionFactory.combine<SecurityCellAction>({
type: SecurityCellActionType.FILTER,
isCompatible: async ({ field }) => fieldHasCellActions(field.name),
execute: async ({ field, metadata }) => {
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
fieldHasCellActions(field.name)
);
},
execute: async ({ data, metadata }) => {
const field = data[0]?.field;
const value = data[0]?.value;
if (!field) return;
// if negateFilters is true we have to perform the opposite operation, we can just execute filterOut with the same params
const addFilter = metadata?.negateFilters === true ? addFilterOut : addFilterIn;
@ -43,13 +55,13 @@ export const createFilterInCellActionFactory = ({
addFilter({
filterManager: timelineFilterManager,
fieldName: field.name,
value: field.value,
value,
});
} else {
addFilter({
filterManager,
fieldName: field.name,
value: field.value,
value,
});
}
},

View file

@ -48,7 +48,12 @@ describe('createFilterOutCellActionFactory', () => {
});
const context = {
field: { name: 'user.name', value: 'the value', type: 'text' },
data: [
{
field: { name: 'user.name', type: 'text' },
value: 'the value',
},
],
} as SecurityCellActionExecutionContext;
it('should return display name', () => {
@ -67,7 +72,11 @@ describe('createFilterOutCellActionFactory', () => {
expect(
await filterOutAction.isCompatible({
...context,
field: { ...context.field, name: 'signal.reason' },
data: [
{
field: { ...context.data[0].field, name: 'signal.reason' },
},
],
})
).toEqual(false);
});

View file

@ -29,8 +29,19 @@ export const createFilterOutCellActionFactory = ({
return genericFilterOutActionFactory.combine<SecurityCellAction>({
type: SecurityCellActionType.FILTER,
isCompatible: async ({ field }) => fieldHasCellActions(field.name),
execute: async ({ field, metadata }) => {
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 && // TODO Add support for multiple values
fieldHasCellActions(field.name)
);
},
execute: async ({ data, metadata }) => {
const field = data[0]?.field;
const value = data[0]?.value;
if (!field) return;
// if negateFilters is true we have to perform the opposite operation, we can just execute filterIn with the same params
const addFilter = metadata?.negateFilters === true ? addFilterIn : addFilterOut;
@ -43,13 +54,13 @@ export const createFilterOutCellActionFactory = ({
addFilter({
filterManager: timelineFilterManager,
fieldName: field.name,
value: field.value,
value,
});
} else {
addFilter({
filterManager,
fieldName: field.name,
value: field.value,
value,
});
}
},

View file

@ -40,7 +40,17 @@ describe('createShowTopNCellActionFactory', () => {
const showTopNAction = showTopNActionFactory({ id: 'testAction' });
const context = {
field: { name: 'user.name', value: 'the-value', type: 'keyword', aggregatable: true },
data: [
{
value: 'the-value',
field: {
name: 'user.name',
type: 'keyword',
aggregatable: true,
searchable: true,
},
},
],
trigger: { id: 'trigger' },
nodeRef: {
current: element,
@ -65,9 +75,16 @@ describe('createShowTopNCellActionFactory', () => {
expect(await showTopNAction.isCompatible(context)).toEqual(true);
});
it('should return false if field type does not support aggregations', async () => {
it('should return false if field esType does not support aggregations', async () => {
expect(
await showTopNAction.isCompatible({ ...context, field: { ...context.field, type: 'text' } })
await showTopNAction.isCompatible({
...context,
data: [
{
field: { ...context.data[0].field, esTypes: ['text'] },
},
],
})
).toEqual(false);
});
@ -75,7 +92,11 @@ describe('createShowTopNCellActionFactory', () => {
expect(
await showTopNAction.isCompatible({
...context,
field: { ...context.field, aggregatable: false },
data: [
{
field: { ...context.data[0].field, aggregatable: false },
},
],
})
).toEqual(false);
});

View file

@ -12,6 +12,7 @@ import { Router } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { ES_FIELD_TYPES } from '@kbn/field-types';
import { KibanaContextProvider } from '../../../common/lib/kibana';
import { APP_NAME, DEFAULT_DARK_MODE } from '../../../../common/constants';
import type { SecurityAppStore } from '../../../common/store';
@ -28,7 +29,7 @@ const SHOW_TOP = (fieldName: string) =>
});
const ICON = 'visBarVertical';
const UNSUPPORTED_FIELD_TYPES = ['date', 'text'];
const UNSUPPORTED_FIELD_TYPES = [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.TEXT];
export const createShowTopNCellActionFactory = createCellActionFactory(
({
@ -42,12 +43,20 @@ export const createShowTopNCellActionFactory = createCellActionFactory(
}): CellActionTemplate<SecurityCellAction> => ({
type: SecurityCellActionType.SHOW_TOP_N,
getIconType: () => ICON,
getDisplayName: ({ field }) => SHOW_TOP(field.name),
getDisplayNameTooltip: ({ field }) => SHOW_TOP(field.name),
isCompatible: async ({ field }) =>
fieldHasCellActions(field.name) &&
!UNSUPPORTED_FIELD_TYPES.includes(field.type) &&
!!field.aggregatable,
getDisplayName: ({ data }) => SHOW_TOP(data[0]?.field.name),
getDisplayNameTooltip: ({ data }) => SHOW_TOP(data[0]?.field.name),
isCompatible: async ({ data }) => {
const field = data[0]?.field;
return (
data.length === 1 &&
fieldHasCellActions(field.name) &&
(field.esTypes ?? []).every(
(esType) => !UNSUPPORTED_FIELD_TYPES.includes(esType as ES_FIELD_TYPES)
) &&
!!field.aggregatable
);
},
execute: async (context) => {
if (!context.nodeRef.current) return;

View file

@ -31,7 +31,17 @@ const element = document.createElement('div');
document.body.appendChild(element);
const context = {
field: { name: 'user.name', value: 'the-value', type: 'keyword' },
data: [
{
value: 'the-value',
field: {
name: 'user.name',
type: 'keyword',
searchable: true,
aggregatable: true,
},
},
],
trigger: { id: 'trigger' },
nodeRef: {
current: element,

View file

@ -10,6 +10,7 @@ import { EuiWrappingPopover } from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import type { CasesUiStart } from '@kbn/cases-plugin/public';
import { first } from 'lodash/fp';
import { StatefulTopN } from '../../common/components/top_n';
import { useGetUserCasesPermissions } from '../../common/lib/kibana';
import { APP_ID } from '../../../common/constants';
@ -29,9 +30,10 @@ export const TopNAction = ({
const { browserFields, indexPattern } = useSourcererDataView(getScopeFromPath(pathname));
const userCasesPermissions = useGetUserCasesPermissions();
const CasesContext = casesService.ui.getCasesContext();
const { field, nodeRef, metadata } = context;
const { data, nodeRef, metadata } = context;
const firstItem = first(data);
if (!nodeRef?.current) return null;
if (!nodeRef?.current || !firstItem) return null;
return (
<CasesContext owner={[APP_ID]} permissions={userCasesPermissions}>
@ -46,11 +48,11 @@ export const TopNAction = ({
attachToAnchor={false}
>
<StatefulTopN
field={field.name}
field={firstItem.field.name}
showLegend
scopeId={metadata?.scopeId}
toggleTopN={onClose}
value={field.value}
value={firstItem.value}
indexPattern={indexPattern}
browserFields={browserFields}
/>

View file

@ -23,7 +23,7 @@ const action = createAction<CellActionExecutionContext>({
getDisplayName: () => displayName,
});
const context = {
field: { name: fieldName, value: fieldValue, type: 'text' },
data: [{ field: { name: fieldName, type: 'text' }, value: fieldValue }],
metadata,
} as CellActionExecutionContext;

View file

@ -22,7 +22,7 @@ export const enhanceActionWithTelemetry = (
telemetry.reportCellActionClicked({
actionId: rest.id,
displayName: rest.getDisplayName(context),
fieldName: context.field.name,
fieldName: context.data.map(({ field }) => field.name).join(', '),
metadata: context.metadata,
});

View file

@ -22,7 +22,12 @@ const store = {
const value = 'the-value';
const fieldName = 'user.name';
const context = {
field: { name: fieldName, value, type: 'text' },
data: [
{
field: { name: fieldName, type: 'text', searchable: true, aggregatable: true },
value,
},
],
metadata: {
scopeId: TableId.test,
},
@ -78,7 +83,10 @@ describe('createToggleColumnCellActionFactory', () => {
it('should add column', async () => {
const name = 'fake-field-name';
await toggleColumnAction.execute({ ...context, field: { ...context.field, name } });
await toggleColumnAction.execute({
...context,
data: [{ ...context.data[0], field: { ...context.data[0].field, name } }],
});
expect(mockDispatch).toHaveBeenCalledWith(
dataTableActions.upsertColumn({
column: {

View file

@ -37,16 +37,21 @@ export const createToggleColumnCellActionFactory = createCellActionFactory(
type: SecurityCellActionType.TOGGLE_COLUMN,
getIconType: () => ICON,
getDisplayName: () => COLUMN_TOGGLE,
getDisplayNameTooltip: ({ field, metadata }) =>
metadata?.isObjectArray ? NESTED_COLUMN(field.name) : COLUMN_TOGGLE,
isCompatible: async ({ field, metadata }) => {
getDisplayNameTooltip: ({ data, metadata }) =>
metadata?.isObjectArray ? NESTED_COLUMN(data[0]?.field.name) : COLUMN_TOGGLE,
isCompatible: async ({ data, metadata }) => {
const field = data[0]?.field;
return (
data.length === 1 &&
fieldHasCellActions(field.name) &&
!!metadata?.scopeId &&
(isTimelineScope(metadata.scopeId) || isInTableScope(metadata.scopeId))
);
},
execute: async ({ metadata, field }) => {
execute: async ({ metadata, data }) => {
const field = data[0]?.field;
const scopeId = metadata?.scopeId;
if (!scopeId) return;

View file

@ -1,36 +0,0 @@
/*
* 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 { CellActions, useDataGridColumnsCellActions } from '@kbn/cell-actions';
import type {
CellActionsProps,
UseDataGridColumnsCellActions,
UseDataGridColumnsCellActionsProps,
} from '@kbn/cell-actions';
import type { SecurityMetadata } from '../../../actions/types';
import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../actions/constants';
// bridge exports for convenience
export * from '@kbn/cell-actions';
export { SecurityCellActionsTrigger, SecurityCellActionType };
export interface SecurityCellActionsProps extends CellActionsProps {
triggerId: string; // can not use SecurityCellActionsTrigger, React.FC Validation throws error for some reason
disabledActionTypes?: string[]; // can not use SecurityCellActionType[], React.FC Validation throws error for some reason
metadata?: SecurityMetadata;
}
export interface UseDataGridColumnsSecurityCellActionsProps
extends UseDataGridColumnsCellActionsProps {
triggerId: SecurityCellActionsTrigger;
disabledActionTypes?: SecurityCellActionType[];
metadata?: SecurityMetadata;
}
// same components with security cell actions types
export const SecurityCellActions: React.FC<SecurityCellActionsProps> = CellActions;
export const useDataGridColumnsSecurityCellActions: UseDataGridColumnsCellActions<UseDataGridColumnsSecurityCellActionsProps> =
useDataGridColumnsCellActions;

View file

@ -0,0 +1,84 @@
/*
* 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 { CellActions, useDataGridColumnsCellActions } from '@kbn/cell-actions';
import type {
CellActionsProps,
UseDataGridColumnsCellActions,
UseDataGridColumnsCellActionsProps,
} from '@kbn/cell-actions';
import React, { useMemo } from 'react';
import type { CellActionFieldValue, CellActionsData } from '@kbn/cell-actions/src/types';
import type { SecurityMetadata } from '../../../actions/types';
import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../actions/constants';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useGetFieldSpec } from '../../hooks/use_get_field_spec';
// bridge exports for convenience
export * from '@kbn/cell-actions';
export { SecurityCellActionsTrigger, SecurityCellActionType };
export interface SecurityCellActionsData {
/**
* The field name is necessary to fetch the FieldSpec from the Dataview.
* Ex: `event.category`
*/
field: string;
value: CellActionFieldValue;
}
export interface SecurityCellActionsProps
extends Omit<CellActionsProps, 'data' | 'metadata' | 'disabledActionTypes' | 'triggerId'> {
sourcererScopeId?: SourcererScopeName;
data: SecurityCellActionsData | SecurityCellActionsData[];
triggerId: SecurityCellActionsTrigger;
disabledActionTypes?: SecurityCellActionType[];
metadata?: SecurityMetadata;
}
export interface UseDataGridColumnsSecurityCellActionsProps
extends UseDataGridColumnsCellActionsProps {
triggerId: SecurityCellActionsTrigger;
disabledActionTypes?: SecurityCellActionType[];
metadata?: SecurityMetadata;
}
export const useDataGridColumnsSecurityCellActions: UseDataGridColumnsCellActions<UseDataGridColumnsSecurityCellActionsProps> =
useDataGridColumnsCellActions;
export const SecurityCellActions: React.FC<SecurityCellActionsProps> = ({
sourcererScopeId = SourcererScopeName.default,
data,
children,
...props
}) => {
const getFieldSpec = useGetFieldSpec(sourcererScopeId);
// Make a dependency key to prevent unnecessary re-renders when data object is defined inline
// It is necessary because the data object is an array or an object and useMemo would always re-render
const dependencyKey = JSON.stringify(data);
const fieldData: CellActionsData[] = useMemo(
() =>
(Array.isArray(data) ? data : [data])
.map(({ field, value }) => ({
field: getFieldSpec(field),
value,
}))
.filter((item): item is CellActionsData => !!item.field),
// eslint-disable-next-line react-hooks/exhaustive-deps -- Use the dependencyKey to prevent unnecessary re-renders
[dependencyKey, getFieldSpec]
);
return fieldData.length > 0 ? (
<CellActions data={fieldData} {...props}>
{children}
</CellActions>
) : (
<>{children}</>
);
};

View file

@ -44,6 +44,8 @@ jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => {
};
});
jest.mock('../../hooks/use_get_field_spec');
const props = {
data: mockAlertDetailsData as TimelineEventsDetailsItem[],
browserFields: mockBrowserFields,

View file

@ -26,6 +26,8 @@ jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => {
};
});
jest.mock('../../hooks/use_get_field_spec');
interface Column {
field: string;
name: string | JSX.Element;

View file

@ -17,6 +17,7 @@ import type { EventFieldsData } from './types';
import type { BrowserField } from '../../../../common/search_strategy';
import { FieldValueCell } from './table/field_value_cell';
import { FieldNameCell } from './table/field_name_cell';
import { getSourcererScopeId } from '../../../helpers';
const HoverActionsContainer = styled(EuiPanel)`
align-items: center;
@ -37,6 +38,7 @@ export const getFieldFromBrowserField = memoizeOne(
get(browserFields, keys),
(newArgs, lastArgs) => newArgs[0].join() === lastArgs[0].join()
);
export const getColumns = ({
browserFields,
eventId,
@ -67,22 +69,16 @@ export const getColumns = ({
truncateText: false,
width: '132px',
render: (values: string[] | null | undefined, data: EventFieldsData) => {
const fieldFromBrowserField = getFieldFromBrowserField(
[data.category, 'fields', data.field],
browserFields
);
return (
<SecurityCellActions
field={{
name: data.field,
data={{
field: data.field,
value: values,
type: data.type,
aggregatable: fieldFromBrowserField?.aggregatable,
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.INLINE}
visibleCellActions={3}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId, isObjectArray: data.isObjectArray }}
/>
);

View file

@ -27,6 +27,7 @@ import type {
} from '../../../../../common/search_strategy';
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view';
import { getSourcererScopeId } from '../../../../helpers';
export interface ThreatSummaryDescription {
browserField: BrowserField;
@ -73,20 +74,8 @@ const EnrichmentDescription: React.FC<ThreatSummaryDescription> = ({
isReadOnly,
}) => {
const metadata = useMemo(() => ({ scopeId }), [scopeId]);
const field = useMemo(
() =>
!data
? null
: {
name: data.field,
value,
type: data.type,
aggregatable: browserField?.aggregatable,
},
[browserField, data, value]
);
if (!data || !value || !field) return null;
if (!data || !value) return null;
const key = `alert-details-value-formatted-field-value-${scopeId}-${eventId}-${data.field}-${value}-${index}-${feedName}`;
return (
@ -115,9 +104,13 @@ const EnrichmentDescription: React.FC<ThreatSummaryDescription> = ({
<EuiFlexItem>
{value && !isReadOnly && (
<SecurityCellActions
field={field}
data={{
field: data.field,
value,
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.INLINE}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={metadata}
visibleCellActions={3}
/>

View file

@ -25,6 +25,8 @@ jest.mock('@elastic/eui', () => {
};
});
jest.mock('../../hooks/use_get_field_spec');
jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => {
const actual = jest.requireActual('@kbn/cell-actions/src/hooks/use_load_actions');
return {

View file

@ -113,24 +113,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `
</div>
<div
class="c4"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-center-row"
data-test-subj="cellActions-renderContent-kibana.alert.workflow_status"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
/>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexGroup inlineActions emotion-euiFlexGroup-none-flexStart-flexStart-row"
data-test-subj="inlineActions"
/>
</div>
</div>
</div>
/>
</div>
</div>
</div>
@ -159,24 +142,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `
</div>
<div
class="c4"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-center-row"
data-test-subj="cellActions-renderContent-kibana.alert.risk_score"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
/>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexGroup inlineActions emotion-euiFlexGroup-none-flexStart-flexStart-row"
data-test-subj="inlineActions"
/>
</div>
</div>
</div>
/>
</div>
</div>
</div>
@ -217,24 +183,7 @@ exports[`Event Details Overview Cards renders rows and spacers correctly 1`] = `
</div>
<div
class="c4"
>
<div
class="euiFlexGroup emotion-euiFlexGroup-none-flexStart-center-row"
data-test-subj="cellActions-renderContent-kibana.alert.rule.name"
>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
/>
<div
class="euiFlexItem emotion-euiFlexItem-growZero"
>
<div
class="euiFlexGroup inlineActions emotion-euiFlexGroup-none-flexStart-flexStart-row"
data-test-subj="inlineActions"
/>
</div>
</div>
</div>
/>
</div>
</div>
</div>

View file

@ -75,6 +75,8 @@ const props = {
jest.mock('../../../lib/kibana');
jest.mock('../../../hooks/use_get_field_spec');
const mockAction = createAction({
id: 'test_action',
execute: async () => {},
@ -82,8 +84,6 @@ const mockAction = createAction({
getDisplayName: () => 'test-actions',
});
// jest.useFakeTimers();
describe('OverviewCardWithActions', () => {
test('it renders correctly', async () => {
await act(async () => {

View file

@ -15,6 +15,7 @@ import {
SecurityCellActionsTrigger,
} from '../../cell_actions';
import type { EnrichedFieldInfo } from '../types';
import { getSourcererScopeId } from '../../../../helpers';
const ActionWrapper = euiStyled.div`
margin-left: ${({ theme }) => theme.eui.euiSizeS};
@ -92,14 +93,13 @@ export const OverviewCardWithActions: React.FC<OverviewCardWithActionsProps> = (
<ActionWrapper>
<SecurityCellActions
field={{
name: enrichedFieldInfo.data.field,
data={{
field: enrichedFieldInfo.data.field,
value: enrichedFieldInfo?.values,
type: enrichedFieldInfo.data.type,
aggregatable: enrichedFieldInfo.fieldFromBrowserField?.aggregatable,
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.INLINE}
sourcererScopeId={getSourcererScopeId(contextId)}
metadata={{ scopeId: contextId }}
visibleCellActions={3}
/>

View file

@ -18,6 +18,8 @@ import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeli
jest.mock('../../../lib/kibana');
jest.mock('../../../hooks/use_get_field_spec');
const eventId = 'TUWyf3wBFCFU0qRJTauW';
const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'];
const hostIpFieldFromBrowserField: BrowserField = {

View file

@ -16,6 +16,7 @@ import { FieldValueCell } from './field_value_cell';
import type { AlertSummaryRow } from '../helpers';
import { hasHoverOrRowActions } from '../helpers';
import { TimelineId } from '../../../../../common/types';
import { getSourcererScopeId } from '../../../../helpers';
const style = { flexGrow: 0 };
@ -45,15 +46,14 @@ export const SummaryValueCell: React.FC<AlertSummaryRow['description']> = ({
/>
{scopeId !== TimelineId.active && !isReadOnly && hoverActionsEnabled && (
<SecurityCellActions
field={{
name: data.field,
data={{
field: data.field,
value: values,
type: data.type,
aggregatable: fieldFromBrowserField?.aggregatable,
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.INLINE}
visibleCellActions={3}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
/>
)}

View file

@ -1,12 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`entity_draggable renders correctly against snapshot 1`] = `
<CellActions
field={
<SecurityCellActions
data={
Object {
"aggregatable": true,
"name": "entity-name",
"type": "keyword",
"field": "entity-name",
"value": "entity-value",
}
}
@ -15,5 +13,5 @@ exports[`entity_draggable renders correctly against snapshot 1`] = `
visibleCellActions={5}
>
entity-name: "entity-value"
</CellActions>
</SecurityCellActions>
`;

View file

@ -16,11 +16,9 @@ interface Props {
export const EntityComponent: React.FC<Props> = ({ entityName, entityValue }) => {
return (
<SecurityCellActions
field={{
name: entityName,
data={{
field: entityName,
value: entityValue,
type: 'keyword',
aggregatable: true,
}}
triggerId={SecurityCellActionsTrigger.DEFAULT}
mode={CellActionsMode.HOVER_DOWN}

View file

@ -1,12 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`draggable_score renders correctly against snapshot 1`] = `
<CellActions
field={
<SecurityCellActions
data={
Object {
"aggregatable": true,
"name": "process.name",
"type": "keyword",
"field": "process.name",
"value": "du",
}
}
@ -15,16 +13,14 @@ exports[`draggable_score renders correctly against snapshot 1`] = `
visibleCellActions={5}
>
17
</CellActions>
</SecurityCellActions>
`;
exports[`draggable_score renders correctly against snapshot when the index is not included 1`] = `
<CellActions
field={
<SecurityCellActions
data={
Object {
"aggregatable": true,
"name": "process.name",
"type": "keyword",
"field": "process.name",
"value": "du",
}
}
@ -33,5 +29,5 @@ exports[`draggable_score renders correctly against snapshot when the index is no
visibleCellActions={5}
>
17
</CellActions>
</SecurityCellActions>
`;

View file

@ -27,11 +27,9 @@ export const ScoreComponent = ({
return (
<SecurityCellActions
mode={CellActionsMode.HOVER_DOWN}
field={{
name: score.entityName,
data={{
value: score.entityValue,
type: 'keyword',
aggregatable: true,
field: score.entityName,
}}
triggerId={SecurityCellActionsTrigger.DEFAULT}
visibleCellActions={5}

View file

@ -37,8 +37,6 @@ export const getAnomaliesHostTableColumns = (
idPrefix: `anomalies-host-table-hostName-${createCompoundAnomalyKey(
anomaliesByHost.anomaly
)}-hostName`,
aggregatable: true,
fieldType: 'keyword',
render: (item) => <HostDetailsLink hostName={item} />,
}),
},

View file

@ -12,7 +12,6 @@ import type { Anomaly, AnomaliesByNetwork } from '../types';
import { getRowItemsWithActions } from '../../tables/helpers';
import { createCompoundAnomalyKey } from './create_compound_key';
import { NetworkDetailsLink } from '../../links';
import * as i18n from './translations';
import { NetworkType } from '../../../../explore/network/store/model';
import type { FlowTarget } from '../../../../../common/search_strategy';
@ -41,8 +40,6 @@ export const getAnomaliesNetworkTableColumns = (
idPrefix: `anomalies-network-table-ip-${createCompoundAnomalyKey(
anomaliesByNetwork.anomaly
)}`,
aggregatable: true,
fieldType: 'ip',
render: (item) => <NetworkDetailsLink ip={item} flowTarget={flowTarget} />,
}),
},

View file

@ -38,8 +38,6 @@ export const getAnomaliesUserTableColumns = (
idPrefix: `anomalies-user-table-userName-${createCompoundAnomalyKey(
anomaliesByUser.anomaly
)}-userName`,
aggregatable: true,
fieldType: 'keyword',
render: (item) => <UserDetailsLink userName={item} />,
}),
},

View file

@ -11,7 +11,6 @@ exports[`Table Helpers #RowItemOverflow it returns correctly against snapshot 1`
>
<MoreContainer
fieldName="attrName"
fieldType="keyword"
idPrefix="idPrefix"
moreMaxHeight="none"
overflowIndexStart={1}

View file

@ -21,6 +21,8 @@ import { render } from '@testing-library/react';
jest.mock('../../lib/kibana');
jest.mock('../../hooks/use_get_field_spec');
describe('Table Helpers', () => {
const items = ['item1', 'item2', 'item3'];
const mount = useMountAppended();
@ -31,7 +33,6 @@ describe('Table Helpers', () => {
values: undefined,
fieldName: 'attrName',
idPrefix: 'idPrefix',
aggregatable: false,
});
const { container } = render(<TestProviders>{rowItem}</TestProviders>);
@ -44,7 +45,6 @@ describe('Table Helpers', () => {
values: [''],
fieldName: 'attrName',
idPrefix: 'idPrefix',
aggregatable: false,
});
const { container } = render(<TestProviders>{rowItem}</TestProviders>);
@ -56,7 +56,6 @@ describe('Table Helpers', () => {
values: null,
fieldName: 'attrName',
idPrefix: 'idPrefix',
aggregatable: false,
displayCount: 0,
});
const { container } = render(<TestProviders>{rowItem}</TestProviders>);
@ -69,7 +68,6 @@ describe('Table Helpers', () => {
values: ['item1'],
fieldName: 'attrName',
idPrefix: 'idPrefix',
aggregatable: false,
render: renderer,
});
const { container } = render(<TestProviders>{rowItem}</TestProviders>);
@ -82,7 +80,6 @@ describe('Table Helpers', () => {
values: [],
fieldName: 'attrName',
idPrefix: 'idPrefix',
aggregatable: false,
});
const { container } = render(<TestProviders>{rowItems}</TestProviders>);
expect(container.textContent).toBe(getEmptyValue());
@ -93,11 +90,12 @@ describe('Table Helpers', () => {
values: items,
fieldName: 'attrName',
idPrefix: 'idPrefix',
aggregatable: false,
displayCount: 2,
});
const { queryAllByTestId, queryByTestId } = render(<TestProviders>{rowItems}</TestProviders>);
const { queryAllByTestId, queryByTestId, debug } = render(
<TestProviders>{rowItems}</TestProviders>
);
debug();
expect(queryAllByTestId('cellActions-renderContent-attrName').length).toBe(2);
expect(queryByTestId('overflow-button')).toBeInTheDocument();
});
@ -112,7 +110,6 @@ describe('Table Helpers', () => {
idPrefix="idPrefix"
maxOverflowItems={1}
overflowIndexStart={1}
fieldType="keyword"
/>
);
expect(wrapper).toMatchSnapshot();
@ -126,7 +123,6 @@ describe('Table Helpers', () => {
idPrefix="idPrefix"
maxOverflowItems={5}
overflowIndexStart={1}
fieldType="keyword"
/>
);
expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(0);
@ -141,7 +137,6 @@ describe('Table Helpers', () => {
idPrefix="idPrefix"
maxOverflowItems={5}
overflowIndexStart={1}
fieldType="keyword"
/>
</TestProviders>
);
@ -161,7 +156,6 @@ describe('Table Helpers', () => {
idPrefix="idPrefix"
maxOverflowItems={1}
overflowIndexStart={1}
fieldType="keyword"
/>
);
expect(wrapper.find('[data-test-subj="popover-additional-overflow"]').length).toBe(1);

View file

@ -21,23 +21,19 @@ const Subtext = styled.div`
interface GetRowItemsWithActionsParams {
values: string[] | null | undefined;
fieldName: string;
fieldType?: string;
idPrefix: string;
render?: (item: string) => JSX.Element;
displayCount?: number;
maxOverflow?: number;
aggregatable: boolean;
}
export const getRowItemsWithActions = ({
values,
fieldName,
fieldType = 'keyword',
idPrefix,
render,
displayCount = 5,
maxOverflow = 5,
aggregatable,
}: GetRowItemsWithActionsParams): JSX.Element => {
if (values != null && values.length > 0) {
const visibleItems = values.slice(0, displayCount).map((value, index) => {
@ -49,11 +45,9 @@ export const getRowItemsWithActions = ({
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: fieldName,
data={{
value,
type: fieldType,
aggregatable,
field: fieldName,
}}
>
<>{render ? render(value) : defaultToEmptyTag(value)}</>
@ -67,11 +61,9 @@ export const getRowItemsWithActions = ({
<RowItemOverflow
fieldName={fieldName}
values={values}
fieldType={fieldType}
idPrefix={idPrefix}
maxOverflowItems={maxOverflow}
overflowIndexStart={displayCount}
isAggregatable={aggregatable}
/>
</>
) : (
@ -84,8 +76,6 @@ export const getRowItemsWithActions = ({
interface RowItemOverflowProps {
fieldName: string;
fieldType: string;
isAggregatable?: boolean;
values: string[];
idPrefix: string;
maxOverflowItems: number;
@ -95,8 +85,6 @@ interface RowItemOverflowProps {
export const RowItemOverflowComponent: React.FC<RowItemOverflowProps> = ({
fieldName,
values,
fieldType,
isAggregatable,
idPrefix,
maxOverflowItems = 5,
overflowIndexStart = 5,
@ -109,8 +97,6 @@ export const RowItemOverflowComponent: React.FC<RowItemOverflowProps> = ({
<MoreContainer
fieldName={fieldName}
idPrefix={idPrefix}
isAggregatable={isAggregatable}
fieldType={fieldType}
values={values}
overflowIndexStart={overflowIndexStart}
moreMaxHeight="none"

View 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const useGetFieldSpec = () => {
return (name: string) => ({
name,
type: 'string',
});
};

View file

@ -0,0 +1,26 @@
/*
* 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 { useCallback } from 'react';
import type { SourcererScopeName } from '../store/sourcerer/model';
import { getSelectedDataviewSelector } from '../store/sourcerer/selectors';
import { useDeepEqualSelector } from './use_selector';
// Calls it from the module scope due to non memoized selectors https://github.com/elastic/kibana/issues/159315
const selectedDataviewSelector = getSelectedDataviewSelector();
export const useGetFieldSpec = (scopeId: SourcererScopeName) => {
const dataView = useDeepEqualSelector((state) => selectedDataviewSelector(state, scopeId));
return useCallback(
(fieldName: string) => {
const fields = dataView?.fields;
return fields && fields[fieldName];
},
[dataView?.fields]
);
};

View file

@ -6,6 +6,7 @@
*/
import { createSelector } from 'reselect';
import type { DataViewSpec } from '@kbn/data-views-plugin/common';
import type { State } from '../types';
import type {
SourcererDataView,
@ -13,7 +14,6 @@ import type {
SourcererScope,
SourcererScopeName,
} from './model';
export const sourcererKibanaDataViewsSelector = ({
sourcerer,
}: State): SourcererModel['kibanaDataViews'] => sourcerer.kibanaDataViews;
@ -97,3 +97,15 @@ export const getSourcererScopeSelector = () => {
};
};
};
export const getSelectedDataviewSelector = () => {
const getSourcererDataViewSelector = sourcererDataViewSelector();
const getScopeSelector = scopeIdSelector();
return (state: State, scopeId: SourcererScopeName): DataViewSpec | undefined => {
const scope = getScopeSelector(state, scopeId);
const selectedDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId);
return selectedDataView?.dataView;
};
};

View file

@ -20,6 +20,7 @@ import { ALERTS_HEADERS_RULE_NAME } from '../../alerts_table/translations';
import { ALERT_TYPE_COLOR, ALERT_TYPE_LABEL } from './helpers';
import { COUNT_TABLE_TITLE } from '../alerts_count_panel/translations';
import * as i18n from './translations';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
export const getAlertsTypeTableColumns = (
isAlertTypeEnabled: boolean
@ -60,11 +61,11 @@ export const getAlertsTypeTableColumns = (
visibleCellActions={4}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'event.type',
data={{
value: 'denied',
type: 'keyword',
field: 'event.type',
}}
sourcererScopeId={SourcererScopeName.detections}
metadata={{ negateFilters: type === 'Detection' }} // Detection: event.type != denied
>
{ALERT_TYPE_LABEL[type as AlertType]}

View file

@ -62,24 +62,30 @@ export const getUseCellActionsHook = (tableId: TableId) => {
tableDefaults.viewMode;
const cellActionProps = useMemo<UseDataGridColumnsSecurityCellActionsProps>(() => {
const fields =
const cellActionsData =
viewMode === VIEW_SELECTION.eventRenderedView
? []
: columns.map((col) => {
const fieldMeta: Partial<BrowserField> | undefined = browserFieldsByName[col.id];
return {
name: col.id,
type: fieldMeta?.type ?? 'keyword',
// 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 ?? []
),
aggregatable: fieldMeta?.aggregatable ?? false,
};
});
return {
triggerId: SecurityCellActionsTrigger.DEFAULT,
fields,
data: cellActionsData,
metadata: {
// cell actions scope
scopeId: tableId,

View file

@ -24,6 +24,7 @@ import {
import { DefaultFieldRenderer } from '../../../../../../timelines/components/field_renderers/field_renderers';
import { HostDetailsLink, NetworkDetailsLink } from '../../../../../../common/components/links';
import type { SelectedDataView } from '../../../../../../common/store/sourcerer/model';
import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model';
import { getEnrichedFieldInfo } from '../../../../../../common/components/event_details/helpers';
import { getTimelineEventData } from '../../../utils/get_timeline_event_data';
import {
@ -170,6 +171,7 @@ export const HostPanel = React.memo(
attrName={'host.ip'}
idPrefix="alert-details-page-user"
render={renderHostIp}
sourcererScopeId={SourcererScopeName.detections}
/>
</HostPanelSection>
</EuiFlexGroup>

View file

@ -21,6 +21,7 @@ import {
import { DefaultFieldRenderer } from '../../../../../../timelines/components/field_renderers/field_renderers';
import { NetworkDetailsLink, UserDetailsLink } from '../../../../../../common/components/links';
import type { SelectedDataView } from '../../../../../../common/store/sourcerer/model';
import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model';
import { getTimelineEventData } from '../../../utils/get_timeline_event_data';
import {
IP_ADDRESSES_TITLE,
@ -135,6 +136,7 @@ export const UserPanel = React.memo(
attrName={'source.ip'}
idPrefix="alert-details-page-user"
render={renderSourceIp}
sourcererScopeId={SourcererScopeName.detections}
/>
</UserPanelSection>
</EuiFlexGroup>

View file

@ -12,7 +12,6 @@ import { getEmptyTagValue } from '../../../common/components/empty_value';
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
import type { Columns, ItemsPerRow } from '../paginated_table';
import { getRowItemsWithActions } from '../../../common/components/tables/helpers';
import * as i18n from './translations';
import {
HostDetailsLink,
@ -102,8 +101,6 @@ const LAST_SUCCESSFUL_SOURCE_COLUMN: Columns<AuthenticationsEdges, Authenticatio
getRowItemsWithActions({
values: node.lastSuccess?.source?.ip || null,
fieldName: 'source.ip',
fieldType: 'ip',
aggregatable: true,
idPrefix: `authentications-table-${node._id}-lastSuccessSource`,
render: (item) => <NetworkDetailsLink ip={item} />,
}),
@ -116,8 +113,6 @@ const LAST_SUCCESSFUL_DESTINATION_COLUMN: Columns<AuthenticationsEdges, Authenti
getRowItemsWithActions({
values: node.lastSuccess?.host?.name ?? null,
fieldName: 'host.name',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `authentications-table-${node._id}-lastSuccessfulDestination`,
render: (item) => <HostDetailsLink hostName={item} />,
}),
@ -141,8 +136,6 @@ const LAST_FAILED_SOURCE_COLUMN: Columns<AuthenticationsEdges, AuthenticationsEd
getRowItemsWithActions({
values: node.lastFailure?.source?.ip || null,
fieldName: 'source.ip',
fieldType: 'ip',
aggregatable: true,
idPrefix: `authentications-table-${node._id}-lastFailureSource`,
render: (item) => <NetworkDetailsLink ip={item} />,
}),
@ -155,8 +148,6 @@ const LAST_FAILED_DESTINATION_COLUMN: Columns<AuthenticationsEdges, Authenticati
getRowItemsWithActions({
values: node.lastFailure?.host?.name || null,
fieldName: 'host.name',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `authentications-table-${node._id}-lastFailureDestination`,
render: (item) => <HostDetailsLink hostName={item} />,
}),
@ -171,8 +162,6 @@ const USER_COLUMN: Columns<AuthenticationsEdges, AuthenticationsEdges> = {
values: node.stackedValue,
fieldName: 'user.name',
idPrefix: `authentications-table-${node._id}-userName`,
fieldType: 'keyword',
aggregatable: true,
render: (item) => <UserDetailsLink userName={item} />,
}),
};
@ -186,8 +175,6 @@ const HOST_COLUMN: Columns<AuthenticationsEdges, AuthenticationsEdges> = {
values: node.stackedValue,
fieldName: 'host.name',
idPrefix: `authentications-table-${node._id}-hostName`,
fieldType: 'keyword',
aggregatable: true,
render: (item) => <HostDetailsLink hostName={item} />,
}),
};

View file

@ -42,11 +42,9 @@ export const getHostRiskScoreColumns = ({
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'host.name',
data={{
value: hostName,
type: 'keyword',
aggregatable: true,
field: 'host.name',
}}
metadata={{
telemetry: CELL_ACTIONS_TELEMETRY,

View file

@ -42,11 +42,9 @@ export const getHostsColumns = (
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'host.name',
data={{
value: hostName[0],
type: 'keyword',
aggregatable: true,
field: 'host.name',
}}
>
<HostDetailsLink hostName={hostName[0]} />
@ -100,10 +98,9 @@ export const getHostsColumns = (
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'host.os.name',
data={{
value: hostOsName[0],
type: 'keyword',
field: 'host.os.name',
}}
>
{hostOsName}
@ -127,10 +124,9 @@ export const getHostsColumns = (
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'host.os.version',
data={{
value: hostOsVersion[0],
type: 'keyword',
field: 'host.os.version',
}}
>
{hostOsVersion}

View file

@ -151,8 +151,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [
getRowItemsWithActions({
values: node.process.name,
fieldName: 'process.name',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `uncommon-process-table-${node._id}-processName`,
}),
},
@ -181,8 +179,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [
getRowItemsWithActions({
values: getHostNames(node.hosts),
fieldName: 'host.name',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `uncommon-process-table-${node._id}-processHost`,
render: (item) => <HostDetailsLink hostName={item} />,
}),
@ -196,8 +192,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [
getRowItemsWithActions({
values: node.process != null ? node.process.args : null,
fieldName: 'process.args',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `uncommon-process-table-${node._id}-processArgs`,
render: (item) => <HostDetailsLink hostName={item} />,
displayCount: 1,
@ -211,8 +205,6 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [
getRowItemsWithActions({
values: node.user != null ? node.user.name : null,
fieldName: 'user.name',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `uncommon-process-table-${node._id}-processUser`,
}),
},

View file

@ -46,11 +46,9 @@ export const getNetworkDnsColumns = (): NetworkDnsColumns => [
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'dns.question.registered_domain',
data={{
value: dnsName,
type: 'keyword',
aggregatable: true,
field: 'dns.question.registered_domain',
}}
>
{defaultToEmptyTag(dnsName)}

View file

@ -38,8 +38,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [
fieldName: 'http.request.method',
values: methods,
idPrefix: escapeDataProviderId(`${tableId}-table-methods-${path}`),
fieldType: 'keyword',
aggregatable: true,
displayCount: 3,
})
: getEmptyTagValue();
@ -53,8 +51,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [
values: domains,
fieldName: 'url.domain',
idPrefix: escapeDataProviderId(`${tableId}-table-domains-${path}`),
fieldType: 'keyword',
aggregatable: true,
})
: getEmptyTagValue(),
},
@ -67,8 +63,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [
values: [path],
fieldName: 'url.path',
idPrefix: escapeDataProviderId(`${tableId}-table-path-${path}`),
fieldType: 'keyword',
aggregatable: true,
})
: getEmptyTagValue(),
},
@ -80,8 +74,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [
values: statuses,
fieldName: 'http.response.status_code',
idPrefix: escapeDataProviderId(`${tableId}-table-statuses-${path}`),
fieldType: 'keyword',
aggregatable: true,
displayCount: 3,
})
: getEmptyTagValue(),
@ -94,8 +86,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [
values: [lastHost],
fieldName: 'host.name',
idPrefix: escapeDataProviderId(`${tableId}-table-lastHost-${path}`),
fieldType: 'keyword',
aggregatable: true,
})
: getEmptyTagValue(),
},
@ -107,8 +97,6 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [
values: [lastSourceIp],
fieldName: 'source.ip',
idPrefix: escapeDataProviderId(`${tableId}-table-lastSourceIp-${path}`),
fieldType: 'keyword',
aggregatable: true,
render: () => <NetworkDetailsLink ip={lastSourceIp} />,
})
: getEmptyTagValue(),

View file

@ -64,11 +64,9 @@ export const getNetworkTopCountriesColumns = (
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: geoAttr,
data={{
value: geo,
type: 'keyword',
aggregatable: true,
field: geoAttr,
}}
>
<CountryFlagAndName countryCode={geo} />

View file

@ -72,11 +72,9 @@ export const getNetworkTopNFlowColumns = (
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: ipAttr,
data={{
value: ip,
type: 'keyword',
aggregatable: true,
field: ipAttr,
}}
>
<NetworkDetailsLink ip={ip} flowTarget={flowTarget} />
@ -89,11 +87,9 @@ export const getNetworkTopNFlowColumns = (
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: geoAttrName,
data={{
value: geo,
type: 'keyword',
aggregatable: true,
field: geoAttrName,
}}
>
{' '}
@ -121,8 +117,6 @@ export const getNetworkTopNFlowColumns = (
return getRowItemsWithActions({
values: domains,
fieldName: domainAttr,
fieldType: 'keyword',
aggregatable: true,
idPrefix: id,
displayCount: 1,
});
@ -145,8 +139,6 @@ export const getNetworkTopNFlowColumns = (
getRowItemsWithActions({
values: [as.name],
fieldName: `${flowTarget}.as.organization.name`,
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${id}-name`,
})}
@ -157,8 +149,6 @@ export const getNetworkTopNFlowColumns = (
values: [`${as.number}`],
fieldName: `${flowTarget}.as.number`,
idPrefix: `${id}-number`,
fieldType: 'keyword',
aggregatable: true,
})}
</>
)}

View file

@ -35,8 +35,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [
getRowItemsWithActions({
values: issuers,
fieldName: 'tls.server.issuer',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-${_id}-table-issuers`,
}),
},
@ -50,8 +48,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [
getRowItemsWithActions({
values: subjects,
fieldName: 'tls.server.subject',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-${_id}-table-subjects`,
}),
},
@ -65,8 +61,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [
getRowItemsWithActions({
values: sha1 ? [sha1] : undefined,
fieldName: 'tls.server.hash.sha1',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-${sha1}-table-sha1`,
}),
},
@ -80,8 +74,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [
getRowItemsWithActions({
values: ja3,
fieldName: 'tls.server.ja3s',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-${_id}-table-ja3`,
}),
},
@ -95,8 +87,6 @@ export const getTlsColumns = (tableId: string): TlsColumns => [
getRowItemsWithActions({
values: notAfter,
fieldName: 'tls.server.not_after',
fieldType: 'date',
aggregatable: false,
idPrefix: `${tableId}-${_id}-table-notAfter`,
render: (validUntil) => (
<LocalizedDateTooltip date={moment(new Date(validUntil)).toDate()}>

View file

@ -34,8 +34,6 @@ export const getUsersColumns = (
getRowItemsWithActions({
values: userName ? [userName] : undefined,
fieldName: 'user.name',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-table-${flowTarget}-user`,
}),
},
@ -49,8 +47,6 @@ export const getUsersColumns = (
getRowItemsWithActions({
values: userIds,
fieldName: 'user.id',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-table-${flowTarget}`,
}),
},
@ -64,8 +60,6 @@ export const getUsersColumns = (
getRowItemsWithActions({
values: groupNames,
fieldName: 'user.group.name',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-table-${flowTarget}`,
}),
},
@ -79,8 +73,6 @@ export const getUsersColumns = (
getRowItemsWithActions({
values: groupId,
fieldName: 'user.group.id',
fieldType: 'keyword',
aggregatable: true,
idPrefix: `${tableId}-table-${flowTarget}`,
}),
},

View file

@ -182,7 +182,10 @@ const NetworkDetailsComponent: React.FC = () => {
}
title={
<SecurityCellActions
field={{ type: 'ip', value: ip, name: `${flowTarget}.ip` }}
data={{
value: ip,
field: `${flowTarget}.ip`,
}}
mode={CellActionsMode.HOVER_DOWN}
visibleCellActions={5}
triggerId={SecurityCellActionsTrigger.DEFAULT}

View file

@ -85,8 +85,6 @@ const getUsersColumns = (
values: [name],
idPrefix: `users-table-${name}-name`,
render: (item) => <UserDetailsLink userName={item} />,
aggregatable: true,
fieldType: 'keyword',
})
: getOrEmptyTagFromValue(name),
},
@ -110,8 +108,6 @@ const getUsersColumns = (
fieldName: 'user.domain',
values: [domain],
idPrefix: `users-table-${domain}-domain`,
aggregatable: true,
fieldType: 'keyword',
})
: getOrEmptyTagFromValue(domain),
},

View file

@ -45,11 +45,9 @@ export const getUserRiskScoreColumns = ({
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'user.name',
data={{
value: userName,
type: 'keyword',
aggregatable: true,
field: 'user.name',
}}
metadata={{
telemetry: CELL_ACTIONS_TELEMETRY,

View file

@ -126,10 +126,9 @@ export const HostDetails: React.FC<HostDetailsProps> = ({ hostName, timestamp })
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'user.name',
data={{
field: 'user.name',
value: user,
type: 'keyword',
}}
>
{user}

View file

@ -126,10 +126,9 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp })
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: 'host.name',
data={{
value: host,
type: 'keyword',
field: 'host.name',
}}
>
{host}
@ -284,5 +283,3 @@ export const UserDetails: React.FC<UserDetailsProps> = ({ userName, timestamp })
</>
);
};
UserDetails.displayName = 'UserDetails';

View file

@ -37,6 +37,7 @@ import type { InspectResponse, StartedSubPlugins, StartServices } from './types'
import { CASES_SUB_PLUGIN_KEY } from './types';
import { timelineActions } from './timelines/store/timeline';
import { TimelineId } from '../common/types';
import { SourcererScopeName } from './common/store/sourcerer/model';
export const parseRoute = (location: Pick<Location, 'hash' | 'pathname' | 'search'>) => {
if (!isEmpty(location.hash)) {
@ -308,6 +309,11 @@ export const isTimelineScope = (scopeId: string) =>
export const isInTableScope = (scopeId: string) =>
Object.values(TableId).includes(scopeId as unknown as TableId);
export const isAlertsPageScope = (scopeId: string) =>
[TableId.alertsOnAlertsPage, TableId.alertsOnRuleDetailsPage, TableId.alertsOnCasePage].includes(
scopeId as TableId
);
export const getScopedActions = (scopeId: string) => {
if (isTimelineScope(scopeId)) {
return timelineActions;
@ -325,3 +331,13 @@ export const getScopedSelectors = (scopeId: string) => {
};
export const isActiveTimeline = (timelineId: string) => timelineId === TimelineId.active;
export const getSourcererScopeId = (scopeId: string): SourcererScopeName => {
if (isTimelineScope(scopeId)) {
return SourcererScopeName.timeline;
} else if (isAlertsPageScope(scopeId)) {
return SourcererScopeName.detections;
} else {
return SourcererScopeName.default;
}
};

View file

@ -27,6 +27,8 @@ jest.mock('../../../../common/hooks/use_global_filter_query', () => {
};
});
jest.mock('../../../../common/hooks/use_get_field_spec');
type UseHostAlertsItemsReturn = ReturnType<UseHostAlertsItems>;
const defaultUseHostAlertsItemsReturn: UseHostAlertsItemsReturn = {
items: [],

View file

@ -39,6 +39,7 @@ import {
SecurityCellActionsTrigger,
} from '../../../../common/components/cell_actions';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
interface HostAlertsTableProps {
signalIndexName: string | null;
@ -152,14 +153,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
'data-test-subj': 'hostSeverityAlertsTable-totalAlerts',
render: (totalAlerts: number, { hostName }) => (
<SecurityCellActions
field={{
name: 'host.name',
data={{
value: hostName,
type: 'keyword',
aggregatable: true,
field: 'host.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
}}
@ -181,14 +181,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="hostSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'host.name',
data={{
value: hostName,
type: 'keyword',
aggregatable: true,
field: 'host.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'critical' },
@ -216,14 +215,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="hostSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'host.name',
data={{
value: hostName,
type: 'keyword',
aggregatable: true,
field: 'host.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'high' },
@ -248,14 +246,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="hostSeverityAlertsTable-medium" color={SEVERITY_COLOR.medium}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'host.name',
data={{
value: hostName,
type: 'keyword',
aggregatable: true,
field: 'host.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'medium' },
@ -280,14 +277,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="hostSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'host.name',
data={{
value: hostName,
type: 'keyword',
aggregatable: true,
field: 'host.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'low' },

View file

@ -40,6 +40,7 @@ import { LastUpdatedAt } from '../../../../common/components/last_updated_at';
import { FormattedCount } from '../../../../common/components/formatted_number';
import { SecurityCellActions } from '../../../../common/components/cell_actions';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
export interface RuleAlertsTableProps {
signalIndexName: string | null;
@ -100,14 +101,13 @@ export const getTableColumns: GetTableColumns = ({
'data-test-subj': 'severityRuleAlertsTable-alertCount',
render: (alertCount: number, { name }) => (
<SecurityCellActions
field={{
name: ALERT_RULE_NAME,
data={{
value: name,
type: 'keyword',
aggregatable: true,
field: ALERT_RULE_NAME,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
}}

View file

@ -26,7 +26,7 @@ import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use
import { FormattedCount } from '../../../../common/components/formatted_number';
import { HeaderSection } from '../../../../common/components/header_section';
import { HoverVisibilityContainer } from '../../../../common/components/hover_visibility_container';
import { BUTTON_CLASS as INPECT_BUTTON_CLASS } from '../../../../common/components/inspect';
import { BUTTON_CLASS as INSPECT_BUTTON_CLASS } from '../../../../common/components/inspect';
import { LastUpdatedAt } from '../../../../common/components/last_updated_at';
import { UserDetailsLink } from '../../../../common/components/links';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
@ -36,6 +36,7 @@ import type { UserAlertsItem } from './use_user_alerts_items';
import { useUserAlertsItems } from './use_user_alerts_items';
import { SecurityCellActions } from '../../../../common/components/cell_actions';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
interface UserAlertsTableProps {
signalIndexName: string | null;
@ -86,7 +87,7 @@ export const UserAlertsTable = React.memo(({ signalIndexName }: UserAlertsTableP
const columns = useMemo(() => getTableColumns(openUserInAlerts), [openUserInAlerts]);
return (
<HoverVisibilityContainer show={true} targetClassNames={[INPECT_BUTTON_CLASS]}>
<HoverVisibilityContainer show={true} targetClassNames={[INSPECT_BUTTON_CLASS]}>
<EuiPanel hasBorder data-test-subj="severityUserAlertsPanel">
<HeaderSection
id={DETECTION_RESPONSE_USER_SEVERITY_QUERY_ID}
@ -149,14 +150,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
'data-test-subj': 'userSeverityAlertsTable-totalAlerts',
render: (totalAlerts: number, { userName }) => (
<SecurityCellActions
field={{
name: 'user.name',
data={{
value: userName,
type: 'keyword',
aggregatable: true,
field: 'user.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [{ field: 'kibana.alert.workflow_status', value: 'open' }],
}}
@ -178,14 +178,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="userSeverityAlertsTable-critical" color={SEVERITY_COLOR.critical}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'user.name',
data={{
value: userName,
type: 'keyword',
aggregatable: true,
field: 'user.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'critical' },
@ -213,14 +212,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="userSeverityAlertsTable-high" color={SEVERITY_COLOR.high}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'user.name',
data={{
value: userName,
type: 'keyword',
aggregatable: true,
field: 'user.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'high' },
@ -245,14 +243,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="userSeverityAlertsTable-medium" color={SEVERITY_COLOR.medium}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'user.name',
data={{
value: userName,
type: 'keyword',
aggregatable: true,
field: 'user.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'medium' },
@ -277,14 +274,13 @@ const getTableColumns: GetTableColumns = (handleClick) => [
<EuiHealth data-test-subj="userSeverityAlertsTable-low" color={SEVERITY_COLOR.low}>
{count > 0 ? (
<SecurityCellActions
field={{
name: 'user.name',
data={{
value: userName,
type: 'keyword',
aggregatable: true,
field: 'user.name',
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}
sourcererScopeId={SourcererScopeName.detections}
metadata={{
andFilters: [
{ field: 'kibana.alert.severity', value: 'low' },

View file

@ -54,10 +54,9 @@ export const getRiskScoreColumns = (
<>
<HostDetailsLink hostName={entityName} hostTab={HostsTableType.risk} />
<StyledCellActions
field={{
name: 'host.name',
data={{
value: entityName,
type: 'keyword',
field: 'host.name',
}}
triggerId={SecurityCellActionsTrigger.DEFAULT}
mode={CellActionsMode.INLINE}
@ -75,10 +74,9 @@ export const getRiskScoreColumns = (
<>
<UserDetailsLink userName={entityName} userTab={UsersTableType.risk} />
<StyledCellActions
field={{
name: 'user.name',
data={{
value: entityName,
type: 'keyword',
field: 'user.name',
}}
triggerId={SecurityCellActionsTrigger.DEFAULT}
mode={CellActionsMode.INLINE}
@ -155,11 +153,9 @@ export const getRiskScoreColumns = (
<FormattedCount count={alertCount} />
</EuiLink>
<StyledCellActions
field={{
name: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
data={{
value: riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name,
type: 'keyword',
aggregatable: true,
field: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name',
}}
mode={CellActionsMode.INLINE}
triggerId={SecurityCellActionsTrigger.ALERTS_COUNT}

View file

@ -17,22 +17,25 @@ import { DefaultFieldRenderer } from '../../../../timelines/components/field_ren
import * as i18n from './translations';
import type { EndpointFields } from '../../../../../common/search_strategy/security_solution/hosts';
import { HostPolicyResponseActionStatus } from '../../../../../common/search_strategy/security_solution/hosts';
import type { SourcererScopeName } from '../../../../common/store/sourcerer/model';
interface Props {
contextID?: string;
data: EndpointFields | null;
sourcererScopeId?: SourcererScopeName;
}
export const EndpointOverview = React.memo<Props>(({ contextID, data }) => {
export const EndpointOverview = React.memo<Props>(({ contextID, data, sourcererScopeId }) => {
const getDefaultRenderer = useCallback(
(fieldName: string, fieldData: EndpointFields, attrName: string) => (
<DefaultFieldRenderer
rowItems={[getOr('', fieldName, fieldData)]}
attrName={attrName}
idPrefix={contextID ? `endpoint-overview-${contextID}` : 'endpoint-overview'}
sourcererScopeId={sourcererScopeId}
/>
),
[contextID]
[contextID, sourcererScopeId]
);
const descriptionLists: Readonly<DescriptionList[][]> = useMemo(() => {
const appliedPolicy = data?.hostInfo?.metadata.Endpoint.policy.applied;

View file

@ -39,9 +39,11 @@ import { OverviewDescriptionList } from '../../../common/components/overview_des
import { useRiskScore } from '../../../explore/containers/risk_score';
import { RiskScore } from '../../../explore/components/risk_score/severity/common';
import { RiskScoreHeaderTitle } from '../../../explore/components/risk_score/risk_score_onboarding/risk_score_header_title';
import type { SourcererScopeName } from '../../../common/store/sourcerer/model';
interface HostSummaryProps {
contextID?: string; // used to provide unique draggable context when viewing in the side panel
sourcererScopeId?: SourcererScopeName;
data: HostItem;
id: string;
isDraggable?: boolean;
@ -66,6 +68,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
({
anomaliesData,
contextID,
sourcererScopeId,
data,
endDate,
id,
@ -109,9 +112,10 @@ export const HostOverview = React.memo<HostSummaryProps>(
attrName={fieldName}
idPrefix={contextID ? `host-overview-${contextID}` : 'host-overview'}
isDraggable={isDraggable}
sourcererScopeId={sourcererScopeId}
/>
),
[contextID, isDraggable]
[contextID, isDraggable, sourcererScopeId]
);
const [hostRiskScore, hostRiskLevel] = useMemo(() => {
@ -229,6 +233,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
rowItems={getOr([], 'host.ip', data)}
attrName={'host.ip'}
idPrefix={contextID ? `host-overview-${contextID}` : 'host-overview'}
sourcererScopeId={sourcererScopeId}
isDraggable={isDraggable}
render={(ip) => (ip != null ? <NetworkDetailsLink ip={ip} /> : getEmptyTagValue())}
/>
@ -265,7 +270,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
},
],
],
[contextID, data, firstColumn, getDefaultRenderer, isDraggable]
[contextID, sourcererScopeId, data, firstColumn, getDefaultRenderer, isDraggable]
);
return (
<>
@ -312,7 +317,11 @@ export const HostOverview = React.memo<HostSummaryProps>(
<>
<EuiHorizontalRule />
<OverviewWrapper direction={isInDetailsSidePanel ? 'column' : 'row'}>
<EndpointOverview contextID={contextID} data={data.endpoint} />
<EndpointOverview
contextID={contextID}
data={data.endpoint}
sourcererScopeId={sourcererScopeId}
/>
{loading && (
<Loader

View file

@ -37,9 +37,11 @@ import { useRiskScore } from '../../../explore/containers/risk_score';
import { RiskScore } from '../../../explore/components/risk_score/severity/common';
import type { UserItem } from '../../../../common/search_strategy/security_solution/users/common';
import { RiskScoreHeaderTitle } from '../../../explore/components/risk_score/risk_score_onboarding/risk_score_header_title';
import type { SourcererScopeName } from '../../../common/store/sourcerer/model';
export interface UserSummaryProps {
contextID?: string; // used to provide unique draggable context when viewing in the side panel
sourcererScopeId?: SourcererScopeName;
data: UserItem;
id: string;
isDraggable?: boolean;
@ -64,6 +66,7 @@ export const UserOverview = React.memo<UserSummaryProps>(
({
anomaliesData,
contextID,
sourcererScopeId,
data,
id,
isDraggable = false,
@ -109,9 +112,10 @@ export const UserOverview = React.memo<UserSummaryProps>(
attrName={fieldName}
idPrefix={contextID ? `user-overview-${contextID}` : 'user-overview'}
isDraggable={isDraggable}
sourcererScopeId={sourcererScopeId}
/>
),
[contextID, isDraggable]
[contextID, isDraggable, sourcererScopeId]
);
const [userRiskScore, userRiskLevel] = useMemo(() => {
@ -243,6 +247,7 @@ export const UserOverview = React.memo<UserSummaryProps>(
rowItems={getOr([], 'host.ip', data)}
attrName={'host.ip'}
idPrefix={contextID ? `user-overview-${contextID}` : 'user-overview'}
sourcererScopeId={sourcererScopeId}
isDraggable={isDraggable}
render={(ip) => (ip != null ? <NetworkDetailsLink ip={ip} /> : getEmptyTagValue())}
/>
@ -250,7 +255,16 @@ export const UserOverview = React.memo<UserSummaryProps>(
},
],
],
[data, indexPatterns, getDefaultRenderer, contextID, isDraggable, userName, firstColumn]
[
data,
indexPatterns,
getDefaultRenderer,
contextID,
sourcererScopeId,
isDraggable,
userName,
firstColumn,
]
);
return (
<>

View file

@ -44,6 +44,8 @@ jest.mock('../../../common/lib/kibana/kibana_react', () => {
};
});
jest.mock('../../../common/hooks/use_get_field_spec');
describe('Field Renderers', () => {
describe('#locationRenderer', () => {
test('it renders correctly against snapshot', () => {
@ -276,7 +278,6 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<MoreContainer
fieldType="keyword"
idPrefix={idPrefix}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={5}
@ -293,7 +294,6 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<MoreContainer
fieldType="keyword"
idPrefix={idPrefix}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={0}
@ -312,7 +312,6 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<MoreContainer
fieldType="keyword"
idPrefix={idPrefix}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={5}
@ -329,7 +328,6 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<MoreContainer
fieldType="keyword"
idPrefix={idPrefix}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={5}
@ -348,7 +346,6 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<MoreContainer
fieldType="keyword"
idPrefix={idPrefix}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={5}
@ -369,7 +366,6 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<MoreContainer
fieldType="keyword"
idPrefix={idPrefix}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={5}
@ -392,9 +388,7 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<DefaultFieldRendererOverflow
fieldType="keyword"
idPrefix={idPrefix}
isAggregatable={true}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={5}
rowItems={rowItems}
@ -413,9 +407,7 @@ describe('Field Renderers', () => {
render(
<TestProviders>
<DefaultFieldRendererOverflow
fieldType="keyword"
idPrefix={idPrefix}
isAggregatable={true}
moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT}
overflowIndexStart={5}
rowItems={rowItems}

View file

@ -30,6 +30,7 @@ import { FormattedRelativePreferenceDate } from '../../../common/components/form
import { HostDetailsLink, ReputationLink, WhoIsLink } from '../../../common/components/links';
import { Spacer } from '../../../common/components/page';
import * as i18n from '../../../explore/network/components/details/translations';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { TimelineContext } from '../timeline';
const DraggableContainerFlexGroup = styled(EuiFlexGroup)`
@ -199,6 +200,7 @@ interface DefaultFieldRendererProps {
moreMaxHeight?: string;
render?: (item: string) => React.ReactNode;
rowItems: string[] | null | undefined;
sourcererScopeId?: SourcererScopeName;
}
export const DefaultFieldRendererComponent: React.FC<DefaultFieldRendererProps> = ({
@ -209,6 +211,7 @@ export const DefaultFieldRendererComponent: React.FC<DefaultFieldRendererProps>
moreMaxHeight = DEFAULT_MORE_MAX_HEIGHT,
render,
rowItems,
sourcererScopeId,
}) => {
if (rowItems != null && rowItems.length > 0) {
const draggables = rowItems.slice(0, displayCount).map((rowItem, index) => {
@ -250,13 +253,12 @@ export const DefaultFieldRendererComponent: React.FC<DefaultFieldRendererProps>
<EuiFlexItem grow={false}>
<DefaultFieldRendererOverflow
attrName={attrName}
fieldType="keyword"
idPrefix={idPrefix}
isAggregatable={true}
moreMaxHeight={moreMaxHeight}
overflowIndexStart={displayCount}
render={render}
rowItems={rowItems}
sourcererScopeId={sourcererScopeId}
/>
</EuiFlexItem>
</DraggableContainerFlexGroup>
@ -274,36 +276,33 @@ DefaultFieldRenderer.displayName = 'DefaultFieldRenderer';
interface DefaultFieldRendererOverflowProps {
attrName: string;
fieldType: string;
rowItems: string[];
idPrefix: string;
isAggregatable?: boolean;
render?: (item: string) => React.ReactNode;
overflowIndexStart?: number;
moreMaxHeight: string;
sourcererScopeId?: SourcererScopeName;
}
interface MoreContainerProps {
fieldName: string;
fieldType: string;
values: string[];
idPrefix: string;
isAggregatable?: boolean;
moreMaxHeight: string;
overflowIndexStart: number;
render?: (item: string) => React.ReactNode;
sourcererScopeId?: SourcererScopeName;
}
export const MoreContainer = React.memo<MoreContainerProps>(
({
fieldName,
fieldType,
idPrefix,
isAggregatable,
moreMaxHeight,
overflowIndexStart,
render,
values,
sourcererScopeId,
}) => {
const { timelineId } = useContext(TimelineContext);
@ -321,12 +320,11 @@ export const MoreContainer = React.memo<MoreContainerProps>(
visibleCellActions={5}
showActionTooltips
triggerId={SecurityCellActionsTrigger.DEFAULT}
field={{
name: fieldName,
data={{
value,
type: fieldType,
aggregatable: isAggregatable,
field: fieldName,
}}
sourcererScopeId={sourcererScopeId ?? SourcererScopeName.default}
metadata={{
scopeId: timelineId ?? undefined,
}}
@ -339,16 +337,7 @@ export const MoreContainer = React.memo<MoreContainerProps>(
return acc;
}, []),
[
fieldName,
fieldType,
idPrefix,
overflowIndexStart,
render,
values,
timelineId,
isAggregatable,
]
[values, overflowIndexStart, idPrefix, fieldName, timelineId, render, sourcererScopeId]
);
return (
@ -377,8 +366,7 @@ export const DefaultFieldRendererOverflow = React.memo<DefaultFieldRendererOverf
overflowIndexStart = 5,
render,
rowItems,
fieldType,
isAggregatable,
sourcererScopeId,
}) => {
const [isOpen, setIsOpen] = useState(false);
const togglePopover = useCallback(() => setIsOpen((currentIsOpen) => !currentIsOpen), []);
@ -420,8 +408,7 @@ export const DefaultFieldRendererOverflow = React.memo<DefaultFieldRendererOverf
values={rowItems}
moreMaxHeight={moreMaxHeight}
overflowIndexStart={overflowIndexStart}
fieldType={fieldType}
isAggregatable={isAggregatable}
sourcererScopeId={sourcererScopeId}
/>
</EuiPopover>
)}

View file

@ -48,6 +48,7 @@ describe('Expandable Host Component', () => {
const mockProps = {
contextID: 'text-context',
hostName: 'testHostName',
scopeId: 'testScopeId',
};
describe('ExpandableHostDetails: rendering', () => {

View file

@ -21,6 +21,7 @@ import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/a
import { hostToCriteria } from '../../../../common/components/ml/criteria/host_to_criteria';
import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime';
import { useHostDetails, ID } from '../../../../explore/hosts/containers/hosts/details';
import { getSourcererScopeId } from '../../../../helpers';
interface ExpandableHostProps {
hostName: string;
@ -53,9 +54,10 @@ export const ExpandableHostDetailsPageLink = ({ hostName }: ExpandableHostProps)
export const ExpandableHostDetails = ({
contextID,
scopeId,
hostName,
isDraggable = false,
}: ExpandableHostProps & { contextID: string; isDraggable?: boolean }) => {
}: ExpandableHostProps & { contextID: string; scopeId: string; isDraggable?: boolean }) => {
const { to, from, isInitializing } = useGlobalTime();
/*
Normally `selectedPatterns` from useSourcererDataView would be where we obtain the indices,
@ -98,6 +100,7 @@ export const ExpandableHostDetails = ({
{({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => (
<HostOverview
contextID={contextID}
sourcererScopeId={getSourcererScopeId(scopeId)}
id={ID}
isInDetailsSidePanel
data={hostOverview as HostItem}

View file

@ -58,6 +58,7 @@ const StyledPanelContent = styled.div`
interface HostDetailsProps {
contextID: string;
scopeId: string;
expandedHost: { hostName: string };
handleOnHostClosed: () => void;
isFlyoutView?: boolean;
@ -65,7 +66,7 @@ interface HostDetailsProps {
}
export const HostDetailsPanel: React.FC<HostDetailsProps> = React.memo(
({ contextID, expandedHost, handleOnHostClosed, isDraggable, isFlyoutView }) => {
({ contextID, scopeId, expandedHost, handleOnHostClosed, isDraggable, isFlyoutView }) => {
const { hostName } = expandedHost;
if (!hostName) {
@ -81,7 +82,7 @@ export const HostDetailsPanel: React.FC<HostDetailsProps> = React.memo(
<EuiSpacer size="m" />
<ExpandableHostDetailsPageLink hostName={hostName} />
<EuiSpacer size="m" />
<ExpandableHostDetails contextID={contextID} hostName={hostName} />
<ExpandableHostDetails contextID={contextID} scopeId={scopeId} hostName={hostName} />
</StyledEuiFlyoutBody>
</>
) : (
@ -111,6 +112,7 @@ export const HostDetailsPanel: React.FC<HostDetailsProps> = React.memo(
<StyledPanelContent>
<ExpandableHostDetails
contextID={contextID}
scopeId={scopeId}
hostName={hostName}
isDraggable={isDraggable}
/>

View file

@ -135,6 +135,7 @@ export const DetailsPanel = React.memo(
handleOnHostClosed={closePanel}
isDraggable={isDraggable}
isFlyoutView={isFlyoutView}
scopeId={scopeId}
/>
);
}
@ -152,6 +153,7 @@ export const DetailsPanel = React.memo(
isDraggable={isDraggable}
isFlyoutView={isFlyoutView}
isNewUserDetailsFlyoutEnable={isNewUserDetailsFlyoutEnable}
scopeId={scopeId}
/>
);
}

View file

@ -27,6 +27,7 @@ import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime';
import { InputsModelId } from '../../../../common/store/inputs/constants';
import { setAbsoluteRangeDatePicker } from '../../../../common/store/inputs/actions';
import { getSourcererScopeId } from '../../../../helpers';
const fieldColumn: EuiBasicTableColumn<ObservedUserTable | ManagedUserTable> = {
name: i18n.FIELD_COLUMN_TITLE,
@ -45,6 +46,7 @@ const fieldColumn: EuiBasicTableColumn<ObservedUserTable | ManagedUserTable> = {
export const getManagedUserTableColumns = (
contextID: string,
scopeId: string,
isDraggable: boolean
): ManagedUsersTableColumns => [
fieldColumn,
@ -58,6 +60,7 @@ export const getManagedUserTableColumns = (
attrName={field}
idPrefix={contextID ? `managedUser-${contextID}` : 'managedUser'}
isDraggable={isDraggable}
sourcererScopeId={getSourcererScopeId(scopeId)}
/>
) : (
defaultToEmptyTag(value)
@ -75,6 +78,7 @@ function isAnomalies(
export const getObservedUserTableColumns = (
contextID: string,
scopeId: string,
isDraggable: boolean
): ObservedUsersTableColumns => [
fieldColumn,
@ -96,6 +100,7 @@ export const getObservedUserTableColumns = (
attrName={field}
idPrefix={contextID ? `observedUser-${contextID}` : 'observedUser'}
isDraggable={isDraggable}
sourcererScopeId={getSourcererScopeId(scopeId)}
/>
);
},

View file

@ -15,6 +15,7 @@ describe('ManagedUser', () => {
const mockProps = {
managedUser: mockManagedUser,
contextID: '',
scopeId: '',
isDraggable: false,
};

View file

@ -34,10 +34,12 @@ import { useAppUrl } from '../../../../common/lib/kibana';
export const ManagedUser = ({
managedUser,
contextID,
scopeId,
isDraggable,
}: {
managedUser: ManagedUserData;
contextID: string;
scopeId: string;
isDraggable: boolean;
}) => {
const { euiTheme } = useEuiTheme();
@ -47,8 +49,8 @@ export const ManagedUser = ({
setManagedDataToggleOpen((isOpen) => !isOpen);
}, [setManagedDataToggleOpen]);
const managedUserTableColumns = useMemo(
() => getManagedUserTableColumns(contextID, isDraggable),
[isDraggable, contextID]
() => getManagedUserTableColumns(contextID, scopeId, isDraggable),
[isDraggable, contextID, scopeId]
);
const { getAppUrl } = useAppUrl();

Some files were not shown because too many files have changed in this diff Show more