Skip to content

Commit

Permalink
[Security Solution] Update CellActions field type to be FieldSpec #15…
Browse files Browse the repository at this point in the history
…7243 (#157834)

issue: #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
#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
#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 #150347 using
`subType` from `fieldSpec`
- [ ] Fix #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](https://github.com/machadoum/kibana/blob/9799dbba27c5baf594357eae0bbfc79b4e7da77c/packages/kbn-field-types/src/types.ts#L22)
you can check all possible ES and KBN types and
[here](https://github.com/machadoum/kibana/blob/9799dbba27c5baf594357eae0bbfc79b4e7da77c/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
  • Loading branch information
machadoum authored Jun 22, 2023
1 parent 3fc1e68 commit 5fb9709
Show file tree
Hide file tree
Showing 112 changed files with 1,006 additions and 588 deletions.
49 changes: 44 additions & 5 deletions packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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([
Expand Down Expand Up @@ -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>
);
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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();
Expand All @@ -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"'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
48 changes: 41 additions & 7 deletions packages/kbn-cell-actions/src/actions/filter/filter_in.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -50,7 +55,11 @@ describe('createFilterInActionFactory', () => {
expect(
await filterInAction.isCompatible({
...context,
field: { ...context.field, name: '' },
data: [
{
field: { ...context.data[0].field, name: '' },
},
],
})
).toEqual(false);
});
Expand All @@ -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,
Expand All @@ -86,15 +100,25 @@ 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 });
});

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,
Expand All @@ -106,15 +130,25 @@ 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 });
});

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 });
});
Expand Down
15 changes: 12 additions & 3 deletions packages/kbn-cell-actions/src/actions/filter/filter_in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
},
})
);
Expand Down
48 changes: 41 additions & 7 deletions packages/kbn-cell-actions/src/actions/filter/filter_out.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -48,7 +53,11 @@ describe('createFilterOutAction', () => {
expect(
await filterOutAction.isCompatible({
...context,
field: { ...context.field, name: '' },
data: [
{
field: { ...context.data[0].field, name: '' },
},
],
})
).toEqual(false);
});
Expand All @@ -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,
Expand All @@ -80,15 +94,25 @@ 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 });
});

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,
Expand All @@ -100,15 +124,25 @@ 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 });
});

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 });
});
Expand Down
16 changes: 13 additions & 3 deletions packages/kbn-cell-actions/src/actions/filter/filter_out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
},
})
Expand Down
Loading

0 comments on commit 5fb9709

Please sign in to comment.