mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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:
parent
3fc1e68146
commit
5fb9709d4c
112 changed files with 1008 additions and 590 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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"'
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
|
|
|
@ -19,7 +19,11 @@ const defaultProps = {
|
|||
visibleCellActions: 4,
|
||||
actionContext: {
|
||||
trigger: { id: 'triggerId' },
|
||||
field: { name: 'fieldName' },
|
||||
data: [
|
||||
{
|
||||
field: { name: 'fieldName' },
|
||||
},
|
||||
],
|
||||
} as CellActionExecutionContext,
|
||||
showActionTooltips: false,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"@kbn/data-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ export type ColumnHeaderOptions = Pick<
|
|||
| 'isResizable'
|
||||
> & {
|
||||
aggregatable?: boolean;
|
||||
searchable?: boolean;
|
||||
category?: string;
|
||||
columnHeaderType: ColumnHeaderType;
|
||||
description?: string | null;
|
||||
|
|
|
@ -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: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -23,6 +23,7 @@ export const defaultHeaders: ColumnHeaderOptions[] = [
|
|||
type: 'date',
|
||||
esTypes: ['date'],
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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] =
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
}) ?? [];
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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}</>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 }}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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} />,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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} />,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
};
|
|
@ -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]
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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`,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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()}>
|
||||
|
|
|
@ -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}`,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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' }],
|
||||
}}
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -48,6 +48,7 @@ describe('Expandable Host Component', () => {
|
|||
const mockProps = {
|
||||
contextID: 'text-context',
|
||||
hostName: 'testHostName',
|
||||
scopeId: 'testScopeId',
|
||||
};
|
||||
|
||||
describe('ExpandableHostDetails: rendering', () => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ describe('ManagedUser', () => {
|
|||
const mockProps = {
|
||||
managedUser: mockManagedUser,
|
||||
contextID: '',
|
||||
scopeId: '',
|
||||
isDraggable: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue