Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SecuritySolutions] Update CellActions to support all types used by Discover #160524

Merged
merged 10 commits into from
Jun 30, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@
import { createCopyToClipboardActionFactory } from './copy_to_clipboard';
import type { CellActionExecutionContext } from '../../types';
import type { NotificationsStart } from '@kbn/core/public';
import { KBN_FIELD_TYPES } from '@kbn/field-types';

const mockSuccessToast = jest.fn();
const mockWarningToast = jest.fn();

const mockCopy = jest.fn((text: string) => true);
jest.mock('copy-to-clipboard', () => (text: string) => mockCopy(text));

describe('Default createCopyToClipboardActionFactory', () => {
const copyToClipboardActionFactory = createCopyToClipboardActionFactory({
notifications: { toasts: { addSuccess: mockSuccessToast } } as unknown as NotificationsStart,
notifications: {
toasts: { addSuccess: mockSuccessToast, addWarning: mockWarningToast },
} as unknown as NotificationsStart,
});
const copyToClipboardAction = copyToClipboardActionFactory({ id: 'testAction' });
const context = {
data: [
{
field: { name: 'user.name', type: 'text' },
field: { name: 'user.name', type: 'string' },
value: 'the value',
},
],
Expand All @@ -45,6 +49,20 @@ describe('Default createCopyToClipboardActionFactory', () => {
it('should return true if everything is okay', async () => {
expect(await copyToClipboardAction.isCompatible(context)).toEqual(true);
});

it('should return false if Kbn type is unsupported', async () => {
expect(
await copyToClipboardAction.isCompatible({
...context,
data: [
{
...context.data[0],
field: { ...context.data[0].field, type: KBN_FIELD_TYPES.NUMBER_RANGE },
},
],
})
).toEqual(false);
});
});

describe('execute', () => {
Expand Down Expand Up @@ -111,5 +129,19 @@ describe('Default createCopyToClipboardActionFactory', () => {
expect(mockCopy).toHaveBeenCalledWith('user.name: true AND false AND true');
expect(mockSuccessToast).toHaveBeenCalled();
});

it('should notify the user when value type is unsupported', async () => {
await copyToClipboardAction.execute({
...context,
data: [
{
...context.data[0],
value: {},
},
],
});
expect(mockCopy).not.toHaveBeenCalled();
expect(mockWarningToast).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,16 @@ import copy from 'copy-to-clipboard';
import { i18n } from '@kbn/i18n';
import type { NotificationsStart } from '@kbn/core/public';
import { isString } from 'lodash/fp';
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import { COPY_CELL_ACTION_TYPE } from '../../constants';
import { createCellActionFactory } from '../factory';
import {
filterOutNullableValues,
isTypeSupportedByDefaultActions,
isValueSupportedByDefaultActions,
valueToArray,
} from '../utils';
import { ACTION_INCOMPATIBLE_VALUE_WARNING } from '../translations';

const ICON = 'copyClipboard';
const COPY_TO_CLIPBOARD = i18n.translate('cellActions.actions.copyToClipboard.displayName', {
Expand All @@ -37,19 +45,24 @@ export const createCopyToClipboardActionFactory = createCellActionFactory(

return (
data.length === 1 && // TODO Add support for multiple values
field.name != null
field.name != null &&
isTypeSupportedByDefaultActions(field.type as KBN_FIELD_TYPES)
);
},
execute: async ({ data }) => {
const field = data[0]?.field;
const value = data[0]?.value;
const rawValue = data[0]?.value;
const value = filterOutNullableValues(valueToArray(rawValue));

let textValue: undefined | string;
if (value != null) {
const valuesArray = Array.isArray(value) ? value : [value];
textValue = valuesArray.map((v) => (isString(v) ? `"${escapeValue(v)}"` : v)).join(' AND ');
if (!isValueSupportedByDefaultActions(value)) {
notifications.toasts.addWarning({
title: ACTION_INCOMPATIBLE_VALUE_WARNING,
});
return;
}
const text = textValue ? `${field.name}: ${textValue}` : field.name;

const textValue = value.map((v) => (isString(v) ? `"${escapeValue(v)}"` : v)).join(' AND ');
const text = textValue !== '' ? `${field.name}: ${textValue}` : field.name;
const isSuccess = copy(text, { debug: true });

if (isSuccess) {
Expand Down
78 changes: 34 additions & 44 deletions packages/kbn-cell-actions/src/actions/filter/create_filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ const booleanValue = true;

describe('createFilter', () => {
it.each([
{ caseName: 'string', caseValue: value },
{ caseName: 'string array', caseValue: [value] },
{ caseName: 'number', caseValue: numberValue, query: numberValue.toString() },
{ caseName: 'number array', caseValue: [numberValue], query: numberValue.toString() },
{ caseName: 'boolean', caseValue: booleanValue, query: booleanValue.toString() },
{ caseName: 'boolean array', caseValue: [booleanValue], query: booleanValue.toString() },
])('should return filter with $caseName value', ({ caseValue, query = value }) => {
expect(createFilter({ key: field, value: caseValue, negate: false })).toEqual({
Expand All @@ -42,11 +39,8 @@ describe('createFilter', () => {
});

it.each([
{ caseName: 'string', caseValue: value },
{ caseName: 'string array', caseValue: [value] },
{ caseName: 'number', caseValue: numberValue, query: numberValue.toString() },
{ caseName: 'number array', caseValue: [numberValue], query: numberValue.toString() },
{ caseName: 'boolean', caseValue: booleanValue, query: booleanValue.toString() },
{ caseName: 'boolean array', caseValue: [booleanValue], query: booleanValue.toString() },
])('should return negate filter with $caseName value', ({ caseValue, query = value }) => {
expect(createFilter({ key: field, value: caseValue, negate: true })).toEqual({
Expand Down Expand Up @@ -93,45 +87,41 @@ describe('createFilter', () => {
});
});

it.each([
{ caseName: 'null', caseValue: null },
{ caseName: 'undefined', caseValue: undefined },
{ caseName: 'empty string', caseValue: '' },
{ caseName: 'empty array', caseValue: [] },
])('should return exist filter with $caseName value', ({ caseValue }) => {
expect(createFilter({ key: field, value: caseValue, negate: false })).toEqual({
query: {
exists: {
field,
it.each([{ caseName: 'empty array', caseValue: [] }])(
'should return exist filter with $caseName value',
({ caseValue }) => {
expect(createFilter({ key: field, value: caseValue, negate: false })).toEqual({
query: {
exists: {
field,
},
},
},
meta: {
key: field,
negate: false,
type: 'exists',
value: 'exists',
},
});
});
meta: {
key: field,
negate: false,
type: 'exists',
value: 'exists',
},
});
}
);

it.each([
{ caseName: 'null', caseValue: null },
{ caseName: 'undefined', caseValue: undefined },
{ caseName: 'empty string', caseValue: '' },
{ caseName: 'empty array', caseValue: [] },
])('should return negate exist filter with $caseName value', ({ caseValue }) => {
expect(createFilter({ key: field, value: caseValue, negate: true })).toEqual({
query: {
exists: {
field,
it.each([{ caseName: 'empty array', caseValue: [] }])(
'should return negate exist filter with $caseName value',
({ caseValue }) => {
expect(createFilter({ key: field, value: caseValue, negate: true })).toEqual({
query: {
exists: {
field,
},
},
},
meta: {
key: field,
negate: true,
type: 'exists',
value: 'exists',
},
});
});
meta: {
key: field,
negate: true,
type: 'exists',
value: 'exists',
},
});
}
);
});
27 changes: 11 additions & 16 deletions packages/kbn-cell-actions/src/actions/filter/create_filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@ import {
type PhraseFilter,
type Filter,
} from '@kbn/es-query';
import { isArray } from 'lodash/fp';
import { CellActionFieldValue } from '../../types';
import { DefaultActionsSupportedValue } from '../types';

export const isEmptyFilterValue = (
value: CellActionFieldValue
): value is null | undefined | never[] =>
value == null || value === '' || (isArray(value) && value.length === 0);
export const isEmptyFilterValue = (value: Array<string | number | boolean>) =>
value.length === 0 || value.every((v) => v === '');

const createExistsFilter = ({ key, negate }: { key: string; negate: boolean }): ExistsFilter => ({
meta: { key, negate, type: FILTERS.EXISTS, value: 'exists' },
Expand Down Expand Up @@ -49,7 +46,7 @@ const createCombinedFilter = ({
key,
negate,
}: {
values: string[] | number[] | boolean[];
values: DefaultActionsSupportedValue;
key: string;
negate: boolean;
}): CombinedFilter => ({
Expand All @@ -68,18 +65,16 @@ export const createFilter = ({
negate,
}: {
key: string;
value: CellActionFieldValue;
value: DefaultActionsSupportedValue;
negate: boolean;
}): Filter => {
if (isEmptyFilterValue(value)) {
if (value.length === 0) {
return createExistsFilter({ key, negate });
}
if (Array.isArray(value)) {
if (value.length > 1) {
return createCombinedFilter({ key, negate, values: value });
} else {
return createPhraseFilter({ key, negate, value: value[0] });
}

if (value.length > 1) {
return createCombinedFilter({ key, negate, values: value });
} else {
return createPhraseFilter({ key, negate, value: value[0] });
}
return createPhraseFilter({ key, negate, value });
};
45 changes: 39 additions & 6 deletions packages/kbn-cell-actions/src/actions/filter/filter_in.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { FilterManager } from '@kbn/data-plugin/public';
import { FilterManager, KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
import { createFilterInActionFactory } from './filter_in';
import { makeActionContext } from '../../mocks/helpers';
import { NotificationsStart } from '@kbn/core-notifications-browser';

const mockFilterManager = { addFilters: jest.fn() } as unknown as FilterManager;

Expand All @@ -20,15 +21,18 @@ jest.mock('./create_filter', () => ({
const fieldName = 'user.name';
const value = 'the value';

const mockWarningToast = jest.fn();

describe('createFilterInActionFactory', () => {
const filterInActionFactory = createFilterInActionFactory({
filterManager: mockFilterManager,
notifications: { toasts: { addWarning: mockWarningToast } } as unknown as NotificationsStart,
});
const filterInAction = filterInActionFactory({ id: 'testAction' });
const context = makeActionContext({
data: [
{
field: { name: fieldName, type: 'text', searchable: true, aggregatable: true },
field: { name: fieldName, type: 'string', searchable: true, aggregatable: true },
value,
},
],
Expand Down Expand Up @@ -57,12 +61,27 @@ describe('createFilterInActionFactory', () => {
...context,
data: [
{
...context.data[0],
field: { ...context.data[0].field, name: '' },
},
],
})
).toEqual(false);
});

it('should return false if Kbn type is unsupported', async () => {
expect(
await filterInAction.isCompatible({
...context,
data: [
{
...context.data[0],
field: { ...context.data[0].field, type: KBN_FIELD_TYPES.MISSING },
},
],
})
).toEqual(false);
});
});

describe('execute', () => {
Expand All @@ -75,7 +94,7 @@ describe('createFilterInActionFactory', () => {
await filterInAction.execute(context);
expect(mockCreateFilter).toHaveBeenCalledWith({
key: fieldName,
value,
value: [value],
negate: false,
});
});
Expand Down Expand Up @@ -107,7 +126,7 @@ describe('createFilterInActionFactory', () => {
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: null, negate: true });
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: true });
});

it('should create negate filter query with undefined value', async () => {
Expand All @@ -122,7 +141,7 @@ describe('createFilterInActionFactory', () => {
});
expect(mockCreateFilter).toHaveBeenCalledWith({
key: fieldName,
value: undefined,
value: [],
negate: true,
});
});
Expand All @@ -137,7 +156,7 @@ describe('createFilterInActionFactory', () => {
},
],
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: '', negate: true });
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [''], negate: true });
});

it('should create negate filter query with empty array value', async () => {
Expand All @@ -152,5 +171,19 @@ describe('createFilterInActionFactory', () => {
});
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: true });
});

it('should notify the user when value type is unsupported', async () => {
await filterInAction.execute({
...context,
data: [
{
...context.data[0],
value: [{}, {}, {}],
},
],
});
expect(mockCreateFilter).not.toHaveBeenCalled();
expect(mockWarningToast).toHaveBeenCalled();
});
});
});
Loading