mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Add dataViewId to filter actions (#177946)
## Summary This PR fixes a bug related to the filter edition: https://github.com/elastic/kibana/issues/164406 Filter actions were missing the `meta.index` value, which needs to be assigned to the dataView id being used. When the filter is edited, the filter component retrieves the index pattern from the dataView saved object. The `meta.index` value has been added to all the "Filter in/out" actions using the `CellActions` metadata object. Thanks @angorayc for catching this and implementing the fix ### Screenshots Before:  After:  --------- Co-authored-by: Angela Chuang <yi-chun.chuang@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Angela Chuang <6295984+angorayc@users.noreply.github.com>
This commit is contained in:
parent
4953a9b158
commit
406b24c6a8
53 changed files with 367 additions and 928 deletions
|
@ -12,6 +12,7 @@ const field = 'field.name';
|
|||
const value = 'the-value';
|
||||
const numberValue = 123;
|
||||
const booleanValue = true;
|
||||
const dataViewId = 'mock-data-view-id';
|
||||
|
||||
describe('createFilter', () => {
|
||||
it.each([
|
||||
|
@ -19,7 +20,7 @@ describe('createFilter', () => {
|
|||
{ caseName: 'number array', caseValue: [numberValue], query: numberValue.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({
|
||||
expect(createFilter({ key: field, value: caseValue, negate: false, dataViewId })).toEqual({
|
||||
meta: {
|
||||
type: 'phrase',
|
||||
key: field,
|
||||
|
@ -27,6 +28,7 @@ describe('createFilter', () => {
|
|||
params: {
|
||||
query,
|
||||
},
|
||||
index: dataViewId,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
|
@ -43,7 +45,7 @@ describe('createFilter', () => {
|
|||
{ caseName: 'number array', caseValue: [numberValue], query: numberValue.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({
|
||||
expect(createFilter({ key: field, value: caseValue, negate: true, dataViewId })).toEqual({
|
||||
meta: {
|
||||
type: 'phrase',
|
||||
key: field,
|
||||
|
@ -51,6 +53,7 @@ describe('createFilter', () => {
|
|||
params: {
|
||||
query,
|
||||
},
|
||||
index: dataViewId,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
|
@ -67,19 +70,20 @@ describe('createFilter', () => {
|
|||
{ caseName: 'negated', negate: true },
|
||||
])('should return combined filter with multiple $caseName values', ({ negate }) => {
|
||||
const value2 = 'the-value2';
|
||||
expect(createFilter({ key: field, value: [value, value2], negate })).toEqual({
|
||||
expect(createFilter({ key: field, value: [value, value2], negate, dataViewId })).toEqual({
|
||||
meta: {
|
||||
type: 'combined',
|
||||
relation: 'AND',
|
||||
key: field,
|
||||
negate,
|
||||
index: dataViewId,
|
||||
params: [
|
||||
{
|
||||
meta: { type: 'phrase', key: field, params: { query: value } },
|
||||
meta: { type: 'phrase', key: field, params: { query: value }, index: dataViewId },
|
||||
query: { match_phrase: { [field]: { query: value } } },
|
||||
},
|
||||
{
|
||||
meta: { type: 'phrase', key: field, params: { query: value2 } },
|
||||
meta: { type: 'phrase', key: field, params: { query: value2 }, index: dataViewId },
|
||||
query: { match_phrase: { [field]: { query: value2 } } },
|
||||
},
|
||||
],
|
||||
|
@ -90,13 +94,14 @@ describe('createFilter', () => {
|
|||
it.each([{ caseName: 'empty array', caseValue: [] }])(
|
||||
'should return exist filter with $caseName value',
|
||||
({ caseValue }) => {
|
||||
expect(createFilter({ key: field, value: caseValue, negate: false })).toEqual({
|
||||
expect(createFilter({ key: field, value: caseValue, negate: false, dataViewId })).toEqual({
|
||||
query: {
|
||||
exists: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
index: dataViewId,
|
||||
key: field,
|
||||
negate: false,
|
||||
type: 'exists',
|
||||
|
@ -109,13 +114,14 @@ describe('createFilter', () => {
|
|||
it.each([{ caseName: 'empty array', caseValue: [] }])(
|
||||
'should return negate exist filter with $caseName value',
|
||||
({ caseValue }) => {
|
||||
expect(createFilter({ key: field, value: caseValue, negate: true })).toEqual({
|
||||
expect(createFilter({ key: field, value: caseValue, negate: true, dataViewId })).toEqual({
|
||||
query: {
|
||||
exists: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
index: dataViewId,
|
||||
key: field,
|
||||
negate: true,
|
||||
type: 'exists',
|
||||
|
|
|
@ -18,8 +18,16 @@ import { DefaultActionsSupportedValue } from '../types';
|
|||
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' },
|
||||
const createExistsFilter = ({
|
||||
key,
|
||||
negate,
|
||||
dataViewId,
|
||||
}: {
|
||||
key: string;
|
||||
negate: boolean;
|
||||
dataViewId?: string;
|
||||
}): ExistsFilter => ({
|
||||
meta: { key, negate, type: FILTERS.EXISTS, value: 'exists', index: dataViewId },
|
||||
query: { exists: { field: key } },
|
||||
});
|
||||
|
||||
|
@ -27,12 +35,15 @@ const createPhraseFilter = ({
|
|||
key,
|
||||
negate,
|
||||
value,
|
||||
dataViewId,
|
||||
}: {
|
||||
value: string | number | boolean;
|
||||
key: string;
|
||||
negate?: boolean;
|
||||
dataViewId?: string;
|
||||
}): PhraseFilter => ({
|
||||
meta: {
|
||||
index: dataViewId,
|
||||
key,
|
||||
negate,
|
||||
type: FILTERS.PHRASE,
|
||||
|
@ -45,17 +56,20 @@ const createCombinedFilter = ({
|
|||
values,
|
||||
key,
|
||||
negate,
|
||||
dataViewId,
|
||||
}: {
|
||||
values: DefaultActionsSupportedValue;
|
||||
key: string;
|
||||
negate: boolean;
|
||||
dataViewId?: string;
|
||||
}): CombinedFilter => ({
|
||||
meta: {
|
||||
index: dataViewId,
|
||||
key,
|
||||
negate,
|
||||
type: FILTERS.COMBINED,
|
||||
relation: BooleanRelation.AND,
|
||||
params: values.map((value) => createPhraseFilter({ key, value })),
|
||||
params: values.map((value) => createPhraseFilter({ key, value, dataViewId })),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -63,18 +77,20 @@ export const createFilter = ({
|
|||
key,
|
||||
value,
|
||||
negate,
|
||||
dataViewId,
|
||||
}: {
|
||||
key: string;
|
||||
value: DefaultActionsSupportedValue;
|
||||
negate: boolean;
|
||||
dataViewId?: string;
|
||||
}): Filter => {
|
||||
if (value.length === 0) {
|
||||
return createExistsFilter({ key, negate });
|
||||
return createExistsFilter({ key, negate, dataViewId });
|
||||
}
|
||||
|
||||
if (value.length > 1) {
|
||||
return createCombinedFilter({ key, negate, values: value });
|
||||
return createCombinedFilter({ key, negate, values: value, dataViewId });
|
||||
} else {
|
||||
return createPhraseFilter({ key, negate, value: value[0] });
|
||||
return createPhraseFilter({ key, negate, value: value[0], dataViewId });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ jest.mock('./create_filter', () => ({
|
|||
|
||||
const fieldName = 'user.name';
|
||||
const value = 'the value';
|
||||
const dataViewId = 'mockDataViewId';
|
||||
|
||||
const mockWarningToast = jest.fn();
|
||||
|
||||
|
@ -96,6 +97,7 @@ describe('createFilterInActionFactory', () => {
|
|||
key: fieldName,
|
||||
value: [value],
|
||||
negate: false,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -113,6 +115,7 @@ describe('createFilterInActionFactory', () => {
|
|||
key: fieldName,
|
||||
value: [value],
|
||||
negate: false,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -126,7 +129,12 @@ describe('createFilterInActionFactory', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: true });
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({
|
||||
key: fieldName,
|
||||
value: [],
|
||||
negate: true,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create negate filter query with undefined value', async () => {
|
||||
|
@ -143,6 +151,7 @@ describe('createFilterInActionFactory', () => {
|
|||
key: fieldName,
|
||||
value: [],
|
||||
negate: true,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -156,7 +165,12 @@ describe('createFilterInActionFactory', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [''], negate: true });
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({
|
||||
key: fieldName,
|
||||
value: [''],
|
||||
negate: true,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create negate filter query with empty array value', async () => {
|
||||
|
@ -169,7 +183,12 @@ describe('createFilterInActionFactory', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: true });
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({
|
||||
key: fieldName,
|
||||
value: [],
|
||||
negate: true,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should notify the user when value type is unsupported', async () => {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { FilterManager, KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
|
||||
import { NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
|
||||
import { createFilter, isEmptyFilterValue } from './create_filter';
|
||||
import { FILTER_CELL_ACTION_TYPE } from '../../constants';
|
||||
import { createCellActionFactory } from '../factory';
|
||||
|
@ -46,13 +47,15 @@ export const createFilterInActionFactory = createCellActionFactory(
|
|||
isTypeSupportedByDefaultActions(field.type as KBN_FIELD_TYPES)
|
||||
);
|
||||
},
|
||||
execute: async ({ data }) => {
|
||||
execute: async ({ data, metadata }) => {
|
||||
const field = data[0]?.field;
|
||||
const rawValue = data[0]?.value;
|
||||
const dataViewId = typeof metadata?.dataViewId === 'string' ? metadata.dataViewId : undefined;
|
||||
|
||||
const value = filterOutNullableValues(valueToArray(rawValue));
|
||||
|
||||
if (isValueSupportedByDefaultActions(value)) {
|
||||
addFilterIn({ filterManager, fieldName: field.name, value });
|
||||
addFilterIn({ filterManager, fieldName: field.name, value, dataViewId });
|
||||
} else {
|
||||
toasts.addWarning({
|
||||
title: ACTION_INCOMPATIBLE_VALUE_WARNING,
|
||||
|
@ -66,16 +69,19 @@ export const addFilterIn = ({
|
|||
filterManager,
|
||||
fieldName,
|
||||
value,
|
||||
dataViewId,
|
||||
}: {
|
||||
filterManager: FilterManager | undefined;
|
||||
fieldName: string;
|
||||
value: DefaultActionsSupportedValue;
|
||||
dataViewId?: string;
|
||||
}) => {
|
||||
if (filterManager != null) {
|
||||
const filter = createFilter({
|
||||
key: fieldName,
|
||||
value,
|
||||
negate: isEmptyFilterValue(value),
|
||||
dataViewId,
|
||||
});
|
||||
filterManager.addFilters(filter);
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ jest.mock('./create_filter', () => ({
|
|||
|
||||
const fieldName = 'user.name';
|
||||
const value = 'the value';
|
||||
const dataViewId = 'mockDataViewId';
|
||||
|
||||
const mockWarningToast = jest.fn();
|
||||
|
||||
|
@ -95,6 +96,7 @@ describe('createFilterOutAction', () => {
|
|||
key: fieldName,
|
||||
value: [value],
|
||||
negate: true,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -112,6 +114,7 @@ describe('createFilterOutAction', () => {
|
|||
key: fieldName,
|
||||
value: [value],
|
||||
negate: true,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -125,7 +128,12 @@ describe('createFilterOutAction', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: false });
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({
|
||||
key: fieldName,
|
||||
value: [],
|
||||
negate: false,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create filter query with undefined value', async () => {
|
||||
|
@ -142,6 +150,7 @@ describe('createFilterOutAction', () => {
|
|||
key: fieldName,
|
||||
value: [],
|
||||
negate: false,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -155,7 +164,12 @@ describe('createFilterOutAction', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [''], negate: false });
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({
|
||||
key: fieldName,
|
||||
value: [''],
|
||||
negate: false,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create negate filter query with empty array value', async () => {
|
||||
|
@ -168,7 +182,12 @@ describe('createFilterOutAction', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({ key: fieldName, value: [], negate: false });
|
||||
expect(mockCreateFilter).toHaveBeenCalledWith({
|
||||
key: fieldName,
|
||||
value: [],
|
||||
negate: false,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should notify the user when value type is unsupported', async () => {
|
||||
|
|
|
@ -39,7 +39,6 @@ export const createFilterOutActionFactory = createCellActionFactory(
|
|||
getDisplayNameTooltip: () => FILTER_OUT,
|
||||
isCompatible: async ({ data }) => {
|
||||
const field = data[0]?.field;
|
||||
|
||||
return (
|
||||
data.length === 1 && // TODO Add support for multiple values
|
||||
!!field.name &&
|
||||
|
@ -47,9 +46,11 @@ export const createFilterOutActionFactory = createCellActionFactory(
|
|||
);
|
||||
},
|
||||
|
||||
execute: async ({ data }) => {
|
||||
execute: async ({ data, metadata }) => {
|
||||
const field = data[0]?.field;
|
||||
const rawValue = data[0]?.value;
|
||||
const dataViewId = typeof metadata?.dataViewId === 'string' ? metadata.dataViewId : undefined;
|
||||
|
||||
const value = filterOutNullableValues(valueToArray(rawValue));
|
||||
|
||||
if (isValueSupportedByDefaultActions(value)) {
|
||||
|
@ -57,6 +58,7 @@ export const createFilterOutActionFactory = createCellActionFactory(
|
|||
filterManager,
|
||||
fieldName: field.name,
|
||||
value,
|
||||
dataViewId,
|
||||
});
|
||||
} else {
|
||||
toasts.addWarning({
|
||||
|
@ -71,16 +73,19 @@ export const addFilterOut = ({
|
|||
filterManager,
|
||||
fieldName,
|
||||
value,
|
||||
dataViewId,
|
||||
}: {
|
||||
filterManager: FilterManager | undefined;
|
||||
fieldName: string;
|
||||
value: DefaultActionsSupportedValue;
|
||||
dataViewId?: string;
|
||||
}) => {
|
||||
if (filterManager != null) {
|
||||
const filter = createFilter({
|
||||
key: fieldName,
|
||||
value,
|
||||
negate: !isEmptyFilterValue(value),
|
||||
dataViewId,
|
||||
});
|
||||
filterManager.addFilters(filter);
|
||||
}
|
||||
|
|
|
@ -39,6 +39,6 @@ export const makeActionContext = (
|
|||
},
|
||||
],
|
||||
nodeRef: {} as MutableRefObject<HTMLElement>,
|
||||
metadata: undefined,
|
||||
metadata: { dataViewId: 'mockDataViewId' },
|
||||
...override,
|
||||
});
|
||||
|
|
|
@ -670,12 +670,14 @@ export const UnifiedDataTable = ({
|
|||
: undefined,
|
||||
[cellActionsTriggerId, isPlainRecord, visibleColumns, dataView]
|
||||
);
|
||||
const cellActionsMetadata = useMemo(() => ({ dataViewId: dataView.id }), [dataView]);
|
||||
|
||||
const columnsCellActions = useDataGridColumnsCellActions({
|
||||
fields: cellActionsFields,
|
||||
getCellValue,
|
||||
triggerId: cellActionsTriggerId,
|
||||
dataGridRef,
|
||||
metadata: cellActionsMetadata,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
|
@ -57,6 +57,7 @@ window.matchMedia = jest.fn().mockImplementation((query) => {
|
|||
removeListener: jest.fn(),
|
||||
};
|
||||
});
|
||||
const dataViewId = 'security-solution-default';
|
||||
|
||||
export const TestCellRenderer: React.FC<DeprecatedCellValueElementProps> = ({ columnId, data }) => (
|
||||
<>
|
||||
|
@ -185,6 +186,7 @@ describe('DataTable', () => {
|
|||
fields: [timestampFieldSpec],
|
||||
getCellValue: expect.any(Function),
|
||||
metadata: {
|
||||
dataViewId,
|
||||
scopeId: 'table-test',
|
||||
},
|
||||
dataGridRef: expect.any(Object),
|
||||
|
|
|
@ -170,8 +170,15 @@ export const DataTableComponent = React.memo<DataTableProps>(
|
|||
const dataTable = useShallowEqualSelector<DataTableModel, DataTableState>(
|
||||
(state) => getDataTable(state, id) ?? tableDefaults
|
||||
);
|
||||
const { columns, selectedEventIds, showCheckboxes, sort, isLoading, defaultColumns } =
|
||||
dataTable;
|
||||
const {
|
||||
columns,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
sort,
|
||||
isLoading,
|
||||
defaultColumns,
|
||||
dataViewId,
|
||||
} = dataTable;
|
||||
|
||||
const columnHeaders = memoizedGetColumnHeaders(columns, browserFields, isEventRenderedView);
|
||||
|
||||
|
@ -339,7 +346,7 @@ export const DataTableComponent = React.memo<DataTableProps>(
|
|||
[dispatch, id]
|
||||
);
|
||||
|
||||
const cellActionsMetadata = useMemo(() => ({ scopeId: id }), [id]);
|
||||
const cellActionsMetadata = useMemo(() => ({ scopeId: id, dataViewId }), [dataViewId, id]);
|
||||
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
|
||||
() =>
|
||||
cellActionsTriggerId
|
||||
|
|
|
@ -59,6 +59,7 @@ const MOCK_DATA = {
|
|||
],
|
||||
pageParams: [undefined],
|
||||
};
|
||||
const MOCK_DATA_VIEW_ID = 'dataViewId';
|
||||
|
||||
jest.mock('../../hooks/use_filter', () => ({
|
||||
useSetFilter: () => ({
|
||||
|
@ -82,6 +83,7 @@ describe('ContainerNameWidget component', () => {
|
|||
globalFilter={GLOBAL_FILTER}
|
||||
groupedBy={CONTAINER_IMAGE_NAME}
|
||||
countBy={ENTRY_LEADER_ENTITY_ID}
|
||||
dataViewId={MOCK_DATA_VIEW_ID}
|
||||
/>
|
||||
));
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ export interface ContainerNameWidgetDeps {
|
|||
globalFilter: GlobalFilter;
|
||||
groupedBy: string;
|
||||
countBy?: string;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
interface FilterButtons {
|
||||
|
@ -57,6 +58,7 @@ interface CopyButtons {
|
|||
}
|
||||
|
||||
export const ContainerNameWidget = ({
|
||||
dataViewId,
|
||||
widgetKey,
|
||||
indexPattern,
|
||||
globalFilter,
|
||||
|
@ -117,6 +119,7 @@ export const ContainerNameWidget = ({
|
|||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: aggData.key as string,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
})
|
||||
|
@ -135,13 +138,14 @@ export const ContainerNameWidget = ({
|
|||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: aggData.key as string,
|
||||
dataViewId,
|
||||
});
|
||||
});
|
||||
})
|
||||
.flat() || [],
|
||||
};
|
||||
return result;
|
||||
}, [data, getFilterForValueButton, getFilterOutValueButton, filterManager]);
|
||||
}, [data?.pages, getFilterForValueButton, dataViewId, filterManager, getFilterOutValueButton]);
|
||||
|
||||
const copyToClipboardButtons = useMemo((): CopyButtons => {
|
||||
const result: CopyButtons = {
|
||||
|
|
|
@ -29,6 +29,8 @@ jest.mock('../container_name_widget', () => ({
|
|||
ContainerNameWidget: () => <div>{'Mock Container Name widget'}</div>,
|
||||
}));
|
||||
|
||||
const dataViewId = 'dataViewId';
|
||||
|
||||
const renderWithRouter = (
|
||||
initialEntries: MemoryRouterProps['initialEntries'] = ['/kubernetes']
|
||||
) => {
|
||||
|
@ -64,6 +66,7 @@ const renderWithRouter = (
|
|||
endDate: '2022-06-09T17:52:15.532Z',
|
||||
}}
|
||||
renderSessionsView={jest.fn()}
|
||||
dataViewId={dataViewId}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
|
|
@ -47,6 +47,7 @@ import {
|
|||
import { ContainerNameWidget } from '../container_name_widget';
|
||||
|
||||
const KubernetesSecurityRoutesComponent = ({
|
||||
dataViewId,
|
||||
filter,
|
||||
indexPattern,
|
||||
globalFilter,
|
||||
|
@ -158,6 +159,7 @@ const KubernetesSecurityRoutesComponent = ({
|
|||
<EuiFlexGroup css={styles.widgetsBottomSpacing}>
|
||||
<EuiFlexItem>
|
||||
<PercentWidget
|
||||
dataViewId={dataViewId}
|
||||
title={
|
||||
<>
|
||||
<EuiText size="xs" css={styles.percentageChartTitle}>
|
||||
|
@ -250,6 +252,7 @@ const KubernetesSecurityRoutesComponent = ({
|
|||
shouldHideFilter: true,
|
||||
},
|
||||
}}
|
||||
dataViewId={dataViewId}
|
||||
groupedBy={ENTRY_LEADER_USER_ID}
|
||||
countBy={ENTRY_LEADER_ENTITY_ID}
|
||||
onReduce={onReduceRootAggs}
|
||||
|
@ -259,6 +262,7 @@ const KubernetesSecurityRoutesComponent = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} css={styles.rightWidgetsGroup}>
|
||||
<ContainerNameWidget
|
||||
dataViewId={dataViewId}
|
||||
widgetKey="containerNameSessions"
|
||||
indexPattern={indexPattern}
|
||||
globalFilter={globalFilter}
|
||||
|
|
|
@ -35,6 +35,8 @@ const DATA_VALUE_MAP = {
|
|||
},
|
||||
};
|
||||
|
||||
const MOCK_DATA_VIEW_ID = 'dataViewId';
|
||||
|
||||
jest.mock('../../hooks/use_filter', () => ({
|
||||
useSetFilter: () => ({
|
||||
getFilterForValueButton: jest.fn(),
|
||||
|
@ -54,6 +56,7 @@ describe('PercentWidget component', () => {
|
|||
<PercentWidget
|
||||
title={TITLE}
|
||||
dataValueMap={DATA_VALUE_MAP}
|
||||
dataViewId={MOCK_DATA_VIEW_ID}
|
||||
widgetKey="percentWidget"
|
||||
globalFilter={GLOBAL_FILTER}
|
||||
groupedBy={ENTRY_LEADER_INTERACTIVE}
|
||||
|
|
|
@ -33,6 +33,7 @@ export interface PercentWidgetDeps {
|
|||
groupedBy: string;
|
||||
countBy?: string;
|
||||
onReduce: (result: AggregateResult) => Record<string, number>;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
interface FilterButtons {
|
||||
|
@ -49,6 +50,7 @@ export const PercentWidget = ({
|
|||
groupedBy,
|
||||
countBy,
|
||||
onReduce,
|
||||
dataViewId,
|
||||
}: PercentWidgetDeps) => {
|
||||
const [hoveredFilter, setHoveredFilter] = useState<number | null>(null);
|
||||
const styles = useStyles();
|
||||
|
@ -92,6 +94,7 @@ export const PercentWidget = ({
|
|||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: [groupedByValue],
|
||||
dataViewId,
|
||||
})
|
||||
);
|
||||
result.filterOutButtons.push(
|
||||
|
@ -104,13 +107,14 @@ export const PercentWidget = ({
|
|||
ownFocus: false,
|
||||
showTooltip: true,
|
||||
value: [groupedByValue],
|
||||
dataViewId,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [dataValueMap, filterManager, getFilterForValueButton, getFilterOutValueButton]);
|
||||
}, [dataValueMap, dataViewId, filterManager, getFilterForValueButton, getFilterOutValueButton]);
|
||||
|
||||
return (
|
||||
<div css={styles.container}>
|
||||
|
|
|
@ -38,6 +38,7 @@ export interface KubernetesSecurityDeps {
|
|||
renderSessionsView: (sessionsFilterQuery: string | undefined) => JSX.Element;
|
||||
indexPattern?: IndexPattern;
|
||||
globalFilter: GlobalFilter;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
export interface KubernetesSecurityStart {
|
||||
|
|
|
@ -8,12 +8,11 @@
|
|||
import type {
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridColumn,
|
||||
EuiDataGridColumnCellActionProps,
|
||||
EuiDataGridControlColumn,
|
||||
} from '@elastic/eui';
|
||||
import type { IFieldSubType } from '@kbn/es-query';
|
||||
import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { ComponentType, JSXElementConstructor, ReactNode } from 'react';
|
||||
import type { ComponentType, JSXElementConstructor } from 'react';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import type { SortColumnTable } from '@kbn/securitysolution-data-table';
|
||||
import type { OnRowSelected, SetEventsDeleted, SetEventsLoading } from '..';
|
||||
|
@ -24,50 +23,6 @@ export type ColumnHeaderType = 'not-filtered' | 'text-filter';
|
|||
/** Uniquely identifies a column */
|
||||
export type ColumnId = string;
|
||||
|
||||
/**
|
||||
* A `DataTableCellAction` function accepts `data`, where each row of data is
|
||||
* represented as a `TimelineNonEcsData[]`. For example, `data[0]` would
|
||||
* contain a `TimelineNonEcsData[]` with the first row of data.
|
||||
*
|
||||
* A `DataTableCellAction` returns a function that has access to all the
|
||||
* `EuiDataGridColumnCellActionProps`, _plus_ access to `data`,
|
||||
* which enables code like the following example to be written:
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => {
|
||||
* const value = getMappedNonEcsValue({
|
||||
* data: data[rowIndex], // access a specific row's values
|
||||
* fieldName: columnId,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <Component onClick={() => alert(`row ${rowIndex} col ${columnId} has value ${value}`)} iconType="heart">
|
||||
* {'Love it'}
|
||||
* </Component>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export type DataTableCellAction = ({
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
header,
|
||||
pageSize,
|
||||
scopeId,
|
||||
closeCellPopover,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
/** each row of data is represented as one TimelineNonEcsData[] */
|
||||
data: TimelineNonEcsData[][];
|
||||
ecsData: Ecs[];
|
||||
header?: ColumnHeaderOptions;
|
||||
pageSize: number;
|
||||
scopeId: string;
|
||||
closeCellPopover?: () => void;
|
||||
}) => (props: EuiDataGridColumnCellActionProps) => ReactNode;
|
||||
|
||||
/** The specification of a column header */
|
||||
export type ColumnHeaderOptions = Pick<
|
||||
EuiDataGridColumn,
|
||||
|
|
|
@ -51,8 +51,10 @@ export const createFilterInCellActionFactory = ({
|
|||
);
|
||||
},
|
||||
execute: async ({ data, metadata }) => {
|
||||
const field = data[0]?.field;
|
||||
const fieldName = data[0]?.field.name;
|
||||
const rawValue = data[0]?.value;
|
||||
const dataViewId = metadata?.dataViewId;
|
||||
|
||||
const value = filterOutNullableValues(valueToArray(rawValue));
|
||||
|
||||
if (!isValueSupportedByDefaultActions(value)) {
|
||||
|
@ -62,7 +64,7 @@ export const createFilterInCellActionFactory = ({
|
|||
return;
|
||||
}
|
||||
|
||||
if (!field) return;
|
||||
if (!fieldName) 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;
|
||||
|
@ -73,17 +75,9 @@ export const createFilterInCellActionFactory = ({
|
|||
TimelineId.active
|
||||
)?.filterManager;
|
||||
|
||||
addFilter({
|
||||
filterManager: timelineFilterManager,
|
||||
fieldName: field.name,
|
||||
value,
|
||||
});
|
||||
addFilter({ filterManager: timelineFilterManager, fieldName, value, dataViewId });
|
||||
} else {
|
||||
addFilter({
|
||||
filterManager,
|
||||
fieldName: field.name,
|
||||
value,
|
||||
});
|
||||
addFilter({ filterManager, fieldName, value, dataViewId });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -52,8 +52,9 @@ export const createFilterOutCellActionFactory = ({
|
|||
);
|
||||
},
|
||||
execute: async ({ data, metadata }) => {
|
||||
const field = data[0]?.field;
|
||||
const fieldName = data[0]?.field.name;
|
||||
const rawValue = data[0]?.value;
|
||||
const dataViewId = metadata?.dataViewId;
|
||||
const value = filterOutNullableValues(valueToArray(rawValue));
|
||||
|
||||
if (!isValueSupportedByDefaultActions(value)) {
|
||||
|
@ -63,7 +64,7 @@ export const createFilterOutCellActionFactory = ({
|
|||
return;
|
||||
}
|
||||
|
||||
if (!field) return;
|
||||
if (!fieldName) 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;
|
||||
|
@ -74,17 +75,9 @@ export const createFilterOutCellActionFactory = ({
|
|||
TimelineId.active
|
||||
)?.filterManager;
|
||||
|
||||
addFilter({
|
||||
filterManager: timelineFilterManager,
|
||||
fieldName: field.name,
|
||||
value,
|
||||
});
|
||||
addFilter({ filterManager: timelineFilterManager, fieldName, value, dataViewId });
|
||||
} else {
|
||||
addFilter({
|
||||
filterManager,
|
||||
fieldName: field.name,
|
||||
value,
|
||||
});
|
||||
addFilter({ filterManager, fieldName, value, dataViewId });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -73,6 +73,8 @@ export const createFilterLensAction = ({
|
|||
execute: async ({ data }) => {
|
||||
const field = data[0]?.columnMeta?.field;
|
||||
const rawValue = data[0]?.value;
|
||||
const mayBeDataViewId = data[0]?.columnMeta?.sourceParams?.indexPatternId;
|
||||
const dataViewId = typeof mayBeDataViewId === 'string' ? mayBeDataViewId : undefined;
|
||||
const value = filterOutNullableValues(valueToArray(rawValue));
|
||||
|
||||
if (!isValueSupportedByDefaultActions(value)) {
|
||||
|
@ -93,7 +95,7 @@ export const createFilterLensAction = ({
|
|||
? timeline.filterManager
|
||||
: dataService.query.filterManager;
|
||||
|
||||
addFilter({ filterManager, fieldName: field, value });
|
||||
addFilter({ filterManager, fieldName: field, value, dataViewId });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -47,6 +47,8 @@ export interface SecurityMetadata extends Record<string, unknown> {
|
|||
* an "and" query to the main data provider
|
||||
*/
|
||||
andFilters?: AndFilter[];
|
||||
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
export interface SecurityCellActionExecutionContext extends CellActionExecutionContext {
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context';
|
||||
import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
|
||||
import { useSourcererDataView } from '../../common/containers/sourcerer';
|
||||
|
||||
export interface SendToTimelineButtonProps {
|
||||
asEmptyButton: boolean;
|
||||
|
@ -59,6 +60,7 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
|
|||
const { showAssistantOverlay } = useAssistantContext();
|
||||
const [isTimelineBottomBarVisible] = useShowTimeline();
|
||||
const { discoverStateContainer } = useDiscoverInTimelineContext();
|
||||
const { dataViewId: timelineDataViewId } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
const isEsqlTabInTimelineDisabled = useIsExperimentalFeatureEnabled('timelineEsqlTabDisabled');
|
||||
|
||||
|
@ -167,6 +169,7 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
|
|||
alias: dataProviders[0].name,
|
||||
key: 'query',
|
||||
value: dataProviders[0].kqlQuery,
|
||||
index: timelineDataViewId ?? undefined,
|
||||
},
|
||||
query: JSON.parse(dataProviders[0].kqlQuery),
|
||||
};
|
||||
|
@ -211,8 +214,9 @@ export const SendToTimelineButton: React.FunctionComponent<SendToTimelineButtonP
|
|||
timeRange,
|
||||
keepDataView,
|
||||
dispatch,
|
||||
clearTimeline,
|
||||
discoverStateContainer,
|
||||
clearTimeline,
|
||||
timelineDataViewId,
|
||||
defaultDataView.id,
|
||||
signalIndexName,
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { SecurityCellActionsTrigger } from '../../../actions/constants';
|
||||
import { CellActionsMode, SecurityCellActions } from '.';
|
||||
import { CellActions } from '@kbn/cell-actions';
|
||||
|
||||
const MockCellActions = CellActions as jest.Mocked<typeof CellActions>;
|
||||
jest.mock('@kbn/cell-actions', () => ({
|
||||
...jest.requireActual('@kbn/cell-actions'),
|
||||
CellActions: jest.fn(() => <div data-test-subj="cell-actions-component" />),
|
||||
}));
|
||||
|
||||
const mockFieldSpec = { someFieldSpec: 'theFieldSpec' };
|
||||
const mockGetFieldSpec = jest.fn((_: string) => mockFieldSpec);
|
||||
const mockUseGetFieldSpec = jest.fn((_: unknown) => mockGetFieldSpec);
|
||||
jest.mock('../../hooks/use_get_field_spec', () => ({
|
||||
useGetFieldSpec: (param: unknown) => mockUseGetFieldSpec(param),
|
||||
}));
|
||||
const mockDataViewId = 'security-default-dataview-id';
|
||||
const mockUseDataViewId = jest.fn((_: unknown) => mockDataViewId);
|
||||
jest.mock('../../hooks/use_data_view_id', () => ({
|
||||
useDataViewId: (param: unknown) => mockUseDataViewId(param),
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
triggerId: SecurityCellActionsTrigger.DEFAULT,
|
||||
mode: CellActionsMode.INLINE,
|
||||
};
|
||||
const mockData = [{ field: 'fieldName', value: 'fieldValue' }];
|
||||
const mockMetadata = { someMetadata: 'value' };
|
||||
|
||||
describe('SecurityCellActions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render CellActions component when data is not empty', () => {
|
||||
const result = render(
|
||||
<SecurityCellActions {...defaultProps} data={mockData}>
|
||||
<div />
|
||||
</SecurityCellActions>
|
||||
);
|
||||
|
||||
expect(result.queryByTestId('cell-actions-component')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render children without CellActions component when data is empty', () => {
|
||||
const result = render(
|
||||
<SecurityCellActions {...defaultProps} data={[]}>
|
||||
<div>{'Test Children'}</div>
|
||||
</SecurityCellActions>
|
||||
);
|
||||
|
||||
expect(result.queryByTestId('cell-actions-component')).not.toBeInTheDocument();
|
||||
expect(result.queryByText('Test Children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render CellActions component with correct props', () => {
|
||||
render(
|
||||
<SecurityCellActions {...defaultProps} data={mockData}>
|
||||
<div />
|
||||
</SecurityCellActions>
|
||||
);
|
||||
|
||||
expect(MockCellActions).toHaveBeenCalledWith(expect.objectContaining(defaultProps), {});
|
||||
});
|
||||
|
||||
it('should render CellActions with the correct field spec in the data', () => {
|
||||
render(
|
||||
<SecurityCellActions {...defaultProps} data={mockData}>
|
||||
<div />
|
||||
</SecurityCellActions>
|
||||
);
|
||||
|
||||
expect(MockCellActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: [{ field: mockFieldSpec, value: 'fieldValue' }],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render CellActions with the correct dataViewId in the metadata', () => {
|
||||
render(
|
||||
<SecurityCellActions {...defaultProps} data={mockData} metadata={mockMetadata}>
|
||||
<div />
|
||||
</SecurityCellActions>
|
||||
);
|
||||
|
||||
expect(MockCellActions).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
metadata: { ...mockMetadata, dataViewId: mockDataViewId },
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -17,6 +17,7 @@ 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';
|
||||
import { useDataViewId } from '../../hooks/use_data_view_id';
|
||||
|
||||
// bridge exports for convenience
|
||||
export * from '@kbn/cell-actions';
|
||||
|
@ -54,10 +55,12 @@ export const useDataGridColumnsSecurityCellActions: UseDataGridColumnsCellAction
|
|||
export const SecurityCellActions: React.FC<SecurityCellActionsProps> = ({
|
||||
sourcererScopeId = SourcererScopeName.default,
|
||||
data,
|
||||
metadata,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const getFieldSpec = useGetFieldSpec(sourcererScopeId);
|
||||
const dataViewId = useDataViewId(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);
|
||||
|
@ -74,8 +77,10 @@ export const SecurityCellActions: React.FC<SecurityCellActionsProps> = ({
|
|||
[dependencyKey, getFieldSpec]
|
||||
);
|
||||
|
||||
const metadataWithDataView = useMemo(() => ({ ...metadata, dataViewId }), [dataViewId, metadata]);
|
||||
|
||||
return fieldData.length > 0 ? (
|
||||
<CellActions data={fieldData} {...props}>
|
||||
<CellActions data={fieldData} metadata={metadataWithDataView} {...props}>
|
||||
{children}
|
||||
</CellActions>
|
||||
) : (
|
||||
|
|
|
@ -13,6 +13,10 @@ import type { DataProvider } from '../../../../common/types/timeline';
|
|||
|
||||
jest.mock('../../lib/kibana');
|
||||
jest.mock('../../hooks/use_selector');
|
||||
const mockUseDataViewId = jest.fn();
|
||||
jest.mock('../../hooks/use_data_view_id', () => ({
|
||||
useDataViewId: (scope: string) => mockUseDataViewId(scope),
|
||||
}));
|
||||
jest.mock('../../containers/sourcerer', () => ({
|
||||
useSourcererDataView: jest.fn().mockReturnValue({ browserFields: {} }),
|
||||
}));
|
||||
|
@ -31,6 +35,7 @@ describe('useHoverActionItems', () => {
|
|||
dataProvider: [{} as DataProvider],
|
||||
defaultFocusedButtonRef: null,
|
||||
field: 'kibana.alert.rule.name',
|
||||
scopeId: 'timeline-test',
|
||||
handleHoverActionClicked: jest.fn(),
|
||||
hideAddToTimeline: false,
|
||||
hideTopN: false,
|
||||
|
@ -87,6 +92,14 @@ describe('useHoverActionItems', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should call getDataViewId with the correct scope', () => {
|
||||
renderHook(() => {
|
||||
const defaultFocusedButtonRef = useRef(null);
|
||||
return useHoverActionItems({ ...defaultProps, defaultFocusedButtonRef });
|
||||
});
|
||||
expect(mockUseDataViewId).toHaveBeenCalledWith('timeline');
|
||||
});
|
||||
|
||||
test('should return overflowActionItems', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { isEmpty } from 'lodash';
|
|||
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isActiveTimeline } from '../../../helpers';
|
||||
import { getSourcererScopeId, isActiveTimeline } from '../../../helpers';
|
||||
import { timelineSelectors } from '../../../timelines/store';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { allowTopN } from '../drag_and_drop/helpers';
|
||||
|
@ -22,6 +22,7 @@ import { TimelineId } from '../../../../common/types/timeline';
|
|||
import { ShowTopNButton } from './actions/show_top_n';
|
||||
import { addProvider } from '../../../timelines/store/actions';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { useDataViewId } from '../../hooks/use_data_view_id';
|
||||
export interface UseHoverActionItemsProps {
|
||||
dataProvider?: DataProvider | DataProvider[];
|
||||
dataType?: string;
|
||||
|
@ -85,6 +86,8 @@ export const useHoverActionItems = ({
|
|||
const kibana = useKibana();
|
||||
const dispatch = useDispatch();
|
||||
const { timelines, uiSettings } = kibana.services;
|
||||
const dataViewId = useDataViewId(getSourcererScopeId(scopeId ?? ''));
|
||||
|
||||
// Common actions used by the alert table and alert flyout
|
||||
const {
|
||||
getAddToTimelineButton,
|
||||
|
@ -193,6 +196,7 @@ export const useHoverActionItems = ({
|
|||
ownFocus,
|
||||
showTooltip: enableOverflowButton ? false : true,
|
||||
value: values,
|
||||
dataViewId,
|
||||
})}
|
||||
</div>
|
||||
) : null,
|
||||
|
@ -207,6 +211,7 @@ export const useHoverActionItems = ({
|
|||
onClick: handleHoverActionClicked,
|
||||
showTooltip: enableOverflowButton ? false : true,
|
||||
value: values,
|
||||
dataViewId,
|
||||
})}
|
||||
</div>
|
||||
) : null,
|
||||
|
@ -294,6 +299,7 @@ export const useHoverActionItems = ({
|
|||
stKeyboardEvent,
|
||||
toggleColumn,
|
||||
values,
|
||||
dataViewId,
|
||||
]
|
||||
) as JSX.Element[];
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { renderHook } from '@testing-library/react-hooks';
|
||||
import { TestProviders } from '../mock';
|
||||
import { SourcererScopeName } from '../store/sourcerer/model';
|
||||
import { DEFAULT_DATA_VIEW_ID } from '../../../common/constants';
|
||||
import { useDataViewId } from './use_data_view_id';
|
||||
import * as sourcererSelectors from '../store/sourcerer/selectors';
|
||||
|
||||
describe('useDataViewId', () => {
|
||||
it.each(Object.values(SourcererScopeName))(
|
||||
'should return the data view id for %s scope',
|
||||
(scope) => {
|
||||
const { result } = renderHook(useDataViewId, { initialProps: scope, wrapper: TestProviders });
|
||||
expect(result.current).toEqual(DEFAULT_DATA_VIEW_ID); // mocked value
|
||||
}
|
||||
);
|
||||
|
||||
it('should return undefined if dataViewId selector returns null', () => {
|
||||
jest
|
||||
.spyOn(sourcererSelectors, 'sourcererScopeSelectedDataViewId')
|
||||
.mockImplementationOnce(() => null);
|
||||
|
||||
const { result } = renderHook(useDataViewId, {
|
||||
initialProps: SourcererScopeName.default,
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current).toEqual(undefined);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { useSelector } from 'react-redux';
|
||||
import { sourcererScopeSelectedDataViewId } from '../store/sourcerer/selectors';
|
||||
import type { SourcererScopeName } from '../store/sourcerer/model';
|
||||
import type { State } from '../store';
|
||||
|
||||
export const useDataViewId = (scopeId: SourcererScopeName): string | undefined => {
|
||||
const dataViewId = useSelector((state: State) =>
|
||||
sourcererScopeSelectedDataViewId(state, scopeId)
|
||||
);
|
||||
return dataViewId ?? undefined;
|
||||
};
|
|
@ -1,60 +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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { getAddToTimelineCellAction } from './add_to_timeline';
|
||||
|
||||
jest.mock('../kibana');
|
||||
|
||||
const mockDispatch = jest.fn;
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
describe('getAddToTimelineCellAction', () => {
|
||||
const sampleData: TimelineNonEcsData = {
|
||||
field: 'fizz',
|
||||
value: ['buzz'],
|
||||
};
|
||||
const testComponent = () => <></>;
|
||||
const componentProps = {
|
||||
colIndex: 1,
|
||||
rowIndex: 1,
|
||||
columnId: 'fizz',
|
||||
Component: testComponent,
|
||||
isExpanded: false,
|
||||
};
|
||||
describe('when data property is', () => {
|
||||
test('undefined', () => {
|
||||
const CellComponent = getAddToTimelineCellAction({ pageSize: 1, data: undefined });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('empty', () => {
|
||||
const CellComponent = getAddToTimelineCellAction({ pageSize: 1, data: [] });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddToTimelineCellActions', () => {
|
||||
const data: TimelineNonEcsData[][] = [[sampleData]];
|
||||
test('should render with data', () => {
|
||||
const AddToTimelineCellComponent = getAddToTimelineCellAction({ pageSize: 1, data });
|
||||
const result = render(<AddToTimelineCellComponent {...componentProps} />);
|
||||
expect(result.getByTestId('test-add-to-timeline')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,126 +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 type { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getPageRowIndex } from '@kbn/securitysolution-data-table';
|
||||
import type { TimelineNonEcsData } from '../../../../common/search_strategy';
|
||||
import type { DataProvider } from '../../../../common/types';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import {
|
||||
EXISTS_OPERATOR,
|
||||
IS_OPERATOR,
|
||||
} from '../../../timelines/components/timeline/data_providers/data_provider';
|
||||
import { escapeDataProviderId } from '../../components/drag_and_drop/helpers';
|
||||
import { EmptyComponent, useKibanaServices } from './helpers';
|
||||
import { addProvider } from '../../../timelines/store/actions';
|
||||
|
||||
export const getAddToTimelineCellAction = ({
|
||||
data,
|
||||
pageSize,
|
||||
closeCellPopover,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[][];
|
||||
pageSize: number;
|
||||
closeCellPopover?: () => void;
|
||||
}) =>
|
||||
data && data.length > 0
|
||||
? function AddToTimeline({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { timelines } = useKibanaServices();
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
const rowData = useMemo(() => {
|
||||
return {
|
||||
data: data[pageRowIndex],
|
||||
fieldName: columnId,
|
||||
};
|
||||
}, [pageRowIndex, columnId]);
|
||||
|
||||
const value = useGetMappedNonEcsValue(rowData);
|
||||
|
||||
const addToTimelineButton = useMemo(
|
||||
() => timelines.getHoverActions().getAddToTimelineButton,
|
||||
[timelines]
|
||||
);
|
||||
|
||||
const dataProvider: DataProvider[] = useMemo(() => {
|
||||
const queryIdPrefix = `${escapeDataProviderId(columnId)}-row-${rowIndex}-col-${columnId}`;
|
||||
if (!value) {
|
||||
return [
|
||||
{
|
||||
and: [],
|
||||
enabled: true,
|
||||
kqlQuery: '',
|
||||
id: `${queryIdPrefix}`,
|
||||
name: '',
|
||||
excluded: true,
|
||||
queryMatch: {
|
||||
field: columnId,
|
||||
value: '',
|
||||
operator: EXISTS_OPERATOR,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return value.map((x) => ({
|
||||
and: [],
|
||||
enabled: true,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
id: `${queryIdPrefix}-val-${x}`,
|
||||
name: x,
|
||||
queryMatch: {
|
||||
field: columnId,
|
||||
value: x,
|
||||
operator: IS_OPERATOR,
|
||||
},
|
||||
}));
|
||||
}, [columnId, rowIndex, value]);
|
||||
|
||||
/*
|
||||
* Add to Timeline button, adds data to dataprovider but does not persists the Timeline
|
||||
* to the server because of following reasons.
|
||||
*
|
||||
* 1. Add to Timeline button performs actions in `timelines` plugin
|
||||
* 2. `timelines` plugin does not have information on how to create/update the timelines in the server
|
||||
* as it is owned by Security Solution
|
||||
* */
|
||||
const handleAddToTimelineAction = useCallback(() => {
|
||||
dispatch(
|
||||
addProvider({
|
||||
id: TimelineId.active,
|
||||
providers: dataProvider,
|
||||
})
|
||||
);
|
||||
if (closeCellPopover) {
|
||||
closeCellPopover();
|
||||
}
|
||||
}, [dataProvider, dispatch]);
|
||||
|
||||
const addToTimelineProps = useMemo(() => {
|
||||
return {
|
||||
Component,
|
||||
dataProvider,
|
||||
field: columnId,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
onClick: handleAddToTimelineAction,
|
||||
timelineType: 'default',
|
||||
};
|
||||
}, [Component, columnId, dataProvider, handleAddToTimelineAction]);
|
||||
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return pageRowIndex >= data.length ? (
|
||||
<>{EmptyComponent}</>
|
||||
) : (
|
||||
<>{addToTimelineButton(addToTimelineProps)}</>
|
||||
);
|
||||
}
|
||||
: EmptyComponent;
|
|
@ -1,50 +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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { getCopyCellAction } from './copy';
|
||||
|
||||
jest.mock('../kibana');
|
||||
|
||||
describe('getCopyCellAction', () => {
|
||||
const sampleData: TimelineNonEcsData = {
|
||||
field: 'fizz',
|
||||
value: ['buzz'],
|
||||
};
|
||||
const testComponent = () => <></>;
|
||||
const componentProps = {
|
||||
colIndex: 1,
|
||||
rowIndex: 1,
|
||||
columnId: 'fizz',
|
||||
Component: testComponent,
|
||||
isExpanded: false,
|
||||
};
|
||||
describe('when data property is', () => {
|
||||
test('undefined', () => {
|
||||
const CellComponent = getCopyCellAction({ pageSize: 1, data: undefined });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('empty', () => {
|
||||
const CellComponent = getCopyCellAction({ pageSize: 1, data: [] });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CopyCellAction', () => {
|
||||
const data: TimelineNonEcsData[][] = [[sampleData]];
|
||||
test('should render with data', () => {
|
||||
const CopyCellAction = getCopyCellAction({ pageSize: 1, data });
|
||||
const result = render(<CopyCellAction {...componentProps} />);
|
||||
expect(result.getByTestId('test-copy-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,61 +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 type { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import { getPageRowIndex } from '@kbn/securitysolution-data-table';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TimelineNonEcsData } from '../../../../common/search_strategy';
|
||||
|
||||
import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import { EmptyComponent, useKibanaServices } from './helpers';
|
||||
|
||||
export const getCopyCellAction = ({
|
||||
data,
|
||||
pageSize,
|
||||
closeCellPopover,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[][];
|
||||
pageSize: number;
|
||||
closeCellPopover?: () => void;
|
||||
}) =>
|
||||
data && data.length > 0
|
||||
? function CopyButton({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) {
|
||||
const { timelines } = useKibanaServices();
|
||||
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
|
||||
const copyButton = useMemo(() => timelines.getHoverActions().getCopyButton, [timelines]);
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
return {
|
||||
data: data[pageRowIndex],
|
||||
fieldName: columnId,
|
||||
};
|
||||
}, [pageRowIndex, columnId]);
|
||||
|
||||
const value = useGetMappedNonEcsValue(rowData);
|
||||
|
||||
const copyButtonProps = useMemo(() => {
|
||||
return {
|
||||
Component,
|
||||
field: columnId,
|
||||
isHoverAction: false,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
onClick: closeCellPopover,
|
||||
};
|
||||
}, [Component, columnId, value]);
|
||||
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return pageRowIndex >= data.length ? (
|
||||
<>{EmptyComponent}</>
|
||||
) : (
|
||||
<>{copyButton(copyButtonProps)}</>
|
||||
);
|
||||
}
|
||||
: EmptyComponent;
|
|
@ -1,91 +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 type { EuiDataGridColumn, EuiDataGridColumnCellAction } from '@elastic/eui';
|
||||
import type { ColumnHeaderType, DataTableCellAction } from '../../../../common/types';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import type {
|
||||
BrowserFields,
|
||||
TimelineNonEcsData,
|
||||
} from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { defaultCellActions } from './default_cell_actions';
|
||||
import { COLUMNS_WITH_LINKS, EmptyComponent } from './helpers';
|
||||
|
||||
describe('default cell actions', () => {
|
||||
const browserFields: BrowserFields = {};
|
||||
const data: TimelineNonEcsData[][] = [[]];
|
||||
const ecsData: Ecs[] = [];
|
||||
const tableId = TableId.test;
|
||||
const pageSize = 10;
|
||||
|
||||
test('columns without any link action (e.g.: signal.status) should return an empty component (not null or data grid would crash)', () => {
|
||||
const columnHeaders = [
|
||||
{
|
||||
category: 'signal',
|
||||
columnHeaderType: 'no-filtered' as ColumnHeaderType,
|
||||
id: 'signal.status',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
initialWidth: 105,
|
||||
},
|
||||
];
|
||||
|
||||
const columnsWithCellActions: EuiDataGridColumn[] = columnHeaders.map((header) => {
|
||||
const buildAction = (dataTableCellAction: DataTableCellAction) =>
|
||||
dataTableCellAction({
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
header: columnHeaders.find((h) => h.id === header.id),
|
||||
pageSize,
|
||||
scopeId: tableId,
|
||||
}) as EuiDataGridColumnCellAction;
|
||||
|
||||
return {
|
||||
...header,
|
||||
cellActions: defaultCellActions?.map(buildAction),
|
||||
};
|
||||
});
|
||||
|
||||
expect(columnsWithCellActions[0]?.cellActions?.length).toEqual(5);
|
||||
});
|
||||
|
||||
const columnHeadersToTest = COLUMNS_WITH_LINKS.map((c) => [
|
||||
{
|
||||
category: 'signal',
|
||||
columnHeaderType: 'no-filtered' as ColumnHeaderType,
|
||||
id: c.columnId,
|
||||
type: c.fieldType,
|
||||
aggregatable: true,
|
||||
initialWidth: 105,
|
||||
},
|
||||
]);
|
||||
|
||||
describe.each(columnHeadersToTest)('columns with a link action', (columnHeaders) => {
|
||||
test(`${columnHeaders.id ?? columnHeaders.type}`, () => {
|
||||
const columnsWithCellActions: EuiDataGridColumn[] = [columnHeaders].map((header) => {
|
||||
const buildAction = (dataTableCellAction: DataTableCellAction) =>
|
||||
dataTableCellAction({
|
||||
browserFields,
|
||||
data,
|
||||
ecsData,
|
||||
header: [columnHeaders].find((h) => h.id === header.id),
|
||||
pageSize,
|
||||
scopeId: tableId,
|
||||
}) as EuiDataGridColumnCellAction;
|
||||
|
||||
return {
|
||||
...header,
|
||||
cellActions: defaultCellActions?.map(buildAction),
|
||||
};
|
||||
});
|
||||
|
||||
expect(columnsWithCellActions[0]?.cellActions?.length).toEqual(5);
|
||||
expect(columnsWithCellActions[0]?.cellActions![4]).not.toEqual(EmptyComponent);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,24 +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 type { DataTableCellAction } from '../../../../common/types';
|
||||
import { getFilterForCellAction } from './filter_for';
|
||||
import { getFilterOutCellAction } from './filter_out';
|
||||
import { getAddToTimelineCellAction } from './add_to_timeline';
|
||||
import { getCopyCellAction } from './copy';
|
||||
import { FieldValueCell } from './field_value';
|
||||
|
||||
export const cellActions: DataTableCellAction[] = [
|
||||
getFilterForCellAction,
|
||||
getFilterOutCellAction,
|
||||
getAddToTimelineCellAction,
|
||||
getCopyCellAction,
|
||||
FieldValueCell,
|
||||
];
|
||||
|
||||
/** the default actions shown in `EuiDataGrid` cells */
|
||||
export const defaultCellActions = [...cellActions];
|
|
@ -1,159 +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 type { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import { head, getOr, get, isEmpty } from 'lodash/fp';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { getPageRowIndex } from '@kbn/securitysolution-data-table';
|
||||
import type { ColumnHeaderOptions } from '../../../../common/types';
|
||||
import type { TimelineNonEcsData } from '../../../../common/search_strategy';
|
||||
import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
import { parseValue } from '../../../timelines/components/timeline/body/renderers/parse_value';
|
||||
import { EmptyComponent, getLinkColumnDefinition } from './helpers';
|
||||
import { getField, getFieldKey } from '../../../helpers';
|
||||
|
||||
const useFormattedFieldProps = ({
|
||||
rowIndex,
|
||||
pageSize,
|
||||
ecsData,
|
||||
columnId,
|
||||
header,
|
||||
data,
|
||||
}: {
|
||||
rowIndex: number;
|
||||
data: TimelineNonEcsData[][];
|
||||
ecsData: Ecs[];
|
||||
header?: ColumnHeaderOptions;
|
||||
columnId: string;
|
||||
pageSize: number;
|
||||
}) => {
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
const ecs = ecsData[pageRowIndex];
|
||||
const link = getLinkColumnDefinition(columnId, header?.type, header?.linkField);
|
||||
const linkField = header?.linkField ? header?.linkField : link?.linkField;
|
||||
const linkValues = header && getOr([], linkField ?? '', ecs);
|
||||
const eventId = (header && get('_id' ?? '', ecs)) || '';
|
||||
const rowData = useMemo(() => {
|
||||
return {
|
||||
data: data[pageRowIndex],
|
||||
fieldName: columnId,
|
||||
};
|
||||
}, [pageRowIndex, columnId, data]);
|
||||
|
||||
const values = useGetMappedNonEcsValue(rowData);
|
||||
const value = parseValue(head(values));
|
||||
const title = values && values.length > 1 ? `${link?.label}: ${value}` : link?.label;
|
||||
// if linkField is defined but link values is empty, it's possible we are trying to look for a column definition for an old event set
|
||||
if (linkField !== undefined && linkValues.length === 0 && values !== undefined) {
|
||||
const normalizedLinkValue = getField(ecs, linkField);
|
||||
const normalizedLinkField = getFieldKey(ecs, linkField);
|
||||
const normalizedColumnId = getFieldKey(ecs, columnId);
|
||||
const normalizedLink = getLinkColumnDefinition(
|
||||
normalizedColumnId,
|
||||
header?.type,
|
||||
normalizedLinkField
|
||||
);
|
||||
return {
|
||||
pageRowIndex,
|
||||
link: normalizedLink,
|
||||
eventId,
|
||||
fieldFormat: header?.format || '',
|
||||
fieldName: normalizedColumnId,
|
||||
fieldType: header?.type || '',
|
||||
value: parseValue(head(normalizedColumnId)),
|
||||
values,
|
||||
title,
|
||||
linkValue: head<string>(normalizedLinkValue),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
pageRowIndex,
|
||||
link,
|
||||
eventId,
|
||||
fieldFormat: header?.format || '',
|
||||
fieldName: columnId,
|
||||
fieldType: header?.type || '',
|
||||
value,
|
||||
values,
|
||||
title,
|
||||
linkValue: head<string>(linkValues),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const FieldValueCell = ({
|
||||
data,
|
||||
ecsData,
|
||||
header,
|
||||
scopeId,
|
||||
pageSize,
|
||||
closeCellPopover,
|
||||
}: {
|
||||
data: TimelineNonEcsData[][];
|
||||
ecsData: Ecs[];
|
||||
header?: ColumnHeaderOptions;
|
||||
scopeId: string;
|
||||
pageSize: number;
|
||||
closeCellPopover?: () => void;
|
||||
}) => {
|
||||
if (header !== undefined) {
|
||||
return function FieldValue({
|
||||
rowIndex,
|
||||
columnId,
|
||||
Component,
|
||||
}: EuiDataGridColumnCellActionProps) {
|
||||
const {
|
||||
pageRowIndex,
|
||||
link,
|
||||
eventId,
|
||||
value,
|
||||
values,
|
||||
title,
|
||||
fieldName,
|
||||
fieldFormat,
|
||||
fieldType,
|
||||
linkValue,
|
||||
} = useFormattedFieldProps({ rowIndex, pageSize, ecsData, columnId, header, data });
|
||||
|
||||
const showEmpty = useMemo(() => {
|
||||
const hasLink = link !== undefined && values && !isEmpty(value);
|
||||
if (pageRowIndex >= data.length) {
|
||||
return true;
|
||||
} else {
|
||||
return hasLink !== true;
|
||||
}
|
||||
}, [link, pageRowIndex, value, values]);
|
||||
|
||||
return showEmpty === false ? (
|
||||
<FormattedFieldValue
|
||||
Component={Component}
|
||||
contextId={`expanded-value-${columnId}-row-${pageRowIndex}-${scopeId}`}
|
||||
eventId={eventId}
|
||||
fieldFormat={fieldFormat}
|
||||
isAggregatable={header.aggregatable ?? false}
|
||||
fieldName={fieldName}
|
||||
fieldType={fieldType}
|
||||
isButton={true}
|
||||
isDraggable={false}
|
||||
value={value}
|
||||
truncate={false}
|
||||
title={title}
|
||||
linkValue={linkValue}
|
||||
onClick={closeCellPopover}
|
||||
/>
|
||||
) : (
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
EmptyComponent
|
||||
);
|
||||
};
|
||||
} else {
|
||||
return EmptyComponent;
|
||||
}
|
||||
};
|
|
@ -1,50 +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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { getFilterForCellAction } from './filter_for';
|
||||
|
||||
jest.mock('../kibana');
|
||||
|
||||
describe('getFilterForCellAction', () => {
|
||||
const sampleData: TimelineNonEcsData = {
|
||||
field: 'fizz',
|
||||
value: ['buzz'],
|
||||
};
|
||||
const testComponent = () => <></>;
|
||||
const componentProps = {
|
||||
colIndex: 1,
|
||||
rowIndex: 1,
|
||||
columnId: 'fizz',
|
||||
Component: testComponent,
|
||||
isExpanded: false,
|
||||
};
|
||||
describe('when data property is', () => {
|
||||
test('undefined', () => {
|
||||
const CellComponent = getFilterForCellAction({ pageSize: 1, data: undefined });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('empty', () => {
|
||||
const CellComponent = getFilterForCellAction({ pageSize: 1, data: [] });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FilterForCellAction', () => {
|
||||
const data: TimelineNonEcsData[][] = [[sampleData]];
|
||||
test('should render with data', () => {
|
||||
const FilterForCellAction = getFilterForCellAction({ pageSize: 1, data });
|
||||
const result = render(<FilterForCellAction {...componentProps} />);
|
||||
expect(result.getByTestId('test-filter-for')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,63 +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 type { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import { getPageRowIndex } from '@kbn/securitysolution-data-table';
|
||||
import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import { EmptyComponent, onFilterAdded, useKibanaServices } from './helpers';
|
||||
|
||||
export const getFilterForCellAction = ({
|
||||
data,
|
||||
pageSize,
|
||||
closeCellPopover,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[][];
|
||||
pageSize: number;
|
||||
closeCellPopover?: () => void;
|
||||
}) =>
|
||||
data && data.length > 0
|
||||
? function FilterFor({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) {
|
||||
const { timelines, filterManager } = useKibanaServices();
|
||||
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
const rowData = useMemo(() => {
|
||||
return {
|
||||
data: data[pageRowIndex],
|
||||
fieldName: columnId,
|
||||
};
|
||||
}, [pageRowIndex, columnId]);
|
||||
|
||||
const value = useGetMappedNonEcsValue(rowData);
|
||||
const filterForButton = useMemo(
|
||||
() => timelines.getHoverActions().getFilterForValueButton,
|
||||
[timelines]
|
||||
);
|
||||
|
||||
const filterForProps = useMemo(() => {
|
||||
return {
|
||||
Component,
|
||||
field: columnId,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
onClick: closeCellPopover,
|
||||
};
|
||||
}, [Component, columnId, filterManager, value]);
|
||||
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return pageRowIndex >= data.length ? (
|
||||
<>{EmptyComponent}</>
|
||||
) : (
|
||||
<>{filterForButton(filterForProps)}</>
|
||||
);
|
||||
}
|
||||
: EmptyComponent;
|
|
@ -1,50 +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 React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { getFilterOutCellAction } from './filter_out';
|
||||
|
||||
jest.mock('../kibana');
|
||||
|
||||
describe('getFilterOutCellAction', () => {
|
||||
const sampleData: TimelineNonEcsData = {
|
||||
field: 'fizz',
|
||||
value: ['buzz'],
|
||||
};
|
||||
const testComponent = () => <></>;
|
||||
const componentProps = {
|
||||
colIndex: 1,
|
||||
rowIndex: 1,
|
||||
columnId: 'fizz',
|
||||
Component: testComponent,
|
||||
isExpanded: false,
|
||||
};
|
||||
describe('when data property is', () => {
|
||||
test('undefined', () => {
|
||||
const CellComponent = getFilterOutCellAction({ pageSize: 1, data: undefined });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
test('empty', () => {
|
||||
const CellComponent = getFilterOutCellAction({ pageSize: 1, data: [] });
|
||||
const result = render(<CellComponent {...componentProps} />);
|
||||
expect(result.container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FilterOutCellAction', () => {
|
||||
const data: TimelineNonEcsData[][] = [[sampleData]];
|
||||
test('should render with data', () => {
|
||||
const FilterOutCellAction = getFilterOutCellAction({ pageSize: 1, data });
|
||||
const result = render(<FilterOutCellAction {...componentProps} />);
|
||||
expect(result.getByTestId('test-filter-out')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,63 +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 type { EuiDataGridColumnCellActionProps } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import { getPageRowIndex } from '@kbn/securitysolution-data-table';
|
||||
import { useGetMappedNonEcsValue } from '../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import { EmptyComponent, onFilterAdded, useKibanaServices } from './helpers';
|
||||
|
||||
export const getFilterOutCellAction = ({
|
||||
data,
|
||||
pageSize,
|
||||
closeCellPopover,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[][];
|
||||
pageSize: number;
|
||||
closeCellPopover?: () => void;
|
||||
}) =>
|
||||
data && data.length > 0
|
||||
? function FilterOut({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) {
|
||||
const { timelines, filterManager } = useKibanaServices();
|
||||
const pageRowIndex = getPageRowIndex(rowIndex, pageSize);
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
return {
|
||||
data: data[pageRowIndex],
|
||||
fieldName: columnId,
|
||||
};
|
||||
}, [pageRowIndex, columnId]);
|
||||
|
||||
const value = useGetMappedNonEcsValue(rowData);
|
||||
|
||||
const filterOutButton = useMemo(
|
||||
() => timelines.getHoverActions().getFilterOutValueButton,
|
||||
[timelines]
|
||||
);
|
||||
|
||||
const filterOutProps = useMemo(() => {
|
||||
return {
|
||||
Component,
|
||||
field: columnId,
|
||||
filterManager,
|
||||
onFilterAdded,
|
||||
ownFocus: false,
|
||||
showTooltip: false,
|
||||
value,
|
||||
onClick: closeCellPopover,
|
||||
};
|
||||
}, [Component, columnId, filterManager, value]);
|
||||
|
||||
// data grid expects each cell action always return an element, it crashes if returns null
|
||||
return pageRowIndex >= data.length ? (
|
||||
<>{EmptyComponent}</>
|
||||
) : (
|
||||
<>{filterOutButton(filterOutProps)}</>
|
||||
);
|
||||
}
|
||||
: EmptyComponent;
|
|
@ -7,20 +7,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SHOW_TOP_VALUES = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.showTopN.showTopValues',
|
||||
{
|
||||
defaultMessage: 'Show top values',
|
||||
}
|
||||
);
|
||||
|
||||
export const HIDE_TOP_VALUES = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.hideTopValues.HideTopValues',
|
||||
{
|
||||
defaultMessage: 'Hide top values',
|
||||
}
|
||||
);
|
||||
|
||||
export const VIEW_HOST_SUMMARY = i18n.translate(
|
||||
'xpack.securitySolution.expandedValue.links.viewHostSummary',
|
||||
{
|
||||
|
|
|
@ -94,6 +94,8 @@ const platinumBaseColumns = [
|
|||
{ columnHeaderType: 'not-filtered', id: 'destination.ip' },
|
||||
];
|
||||
|
||||
const dataViewId = 'security-solution-default';
|
||||
|
||||
describe('alerts default_config', () => {
|
||||
describe('buildAlertsRuleIdFilter', () => {
|
||||
test('given a rule id this will return an array with a single filter', () => {
|
||||
|
@ -102,6 +104,7 @@ describe('alerts default_config', () => {
|
|||
meta: {
|
||||
alias: null,
|
||||
negate: false,
|
||||
index: dataViewId,
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: 'kibana.alert.rule.rule_id',
|
||||
|
@ -126,6 +129,7 @@ describe('alerts default_config', () => {
|
|||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
index: dataViewId,
|
||||
negate: false,
|
||||
key: 'kibana.alert.rule.type',
|
||||
type: 'term',
|
||||
|
@ -249,6 +253,7 @@ describe('alerts default_config', () => {
|
|||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
index: dataViewId,
|
||||
negate: false,
|
||||
},
|
||||
query: {
|
||||
|
|
|
@ -105,6 +105,7 @@ export const buildAlertsFilter = (ruleStaticId: string | null): Filter[] =>
|
|||
meta: {
|
||||
alias: null,
|
||||
negate: false,
|
||||
index: 'security-solution-default',
|
||||
disabled: false,
|
||||
type: 'phrase',
|
||||
key: ALERT_RULE_RULE_ID,
|
||||
|
@ -133,6 +134,7 @@ export const buildShowBuildingBlockFilter = (showBuildingBlockAlerts: boolean):
|
|||
type: 'exists',
|
||||
key: ALERT_BUILDING_BLOCK_TYPE,
|
||||
value: 'exists',
|
||||
index: 'security-solution-default',
|
||||
},
|
||||
query: { exists: { field: ALERT_BUILDING_BLOCK_TYPE } },
|
||||
},
|
||||
|
@ -148,6 +150,7 @@ export const buildThreatMatchFilter = (showOnlyThreatIndicatorAlerts: boolean):
|
|||
negate: false,
|
||||
key: 'kibana.alert.rule.type',
|
||||
type: 'term',
|
||||
index: 'security-solution-default',
|
||||
},
|
||||
query: { term: { 'kibana.alert.rule.type': 'threat_match' } },
|
||||
},
|
||||
|
@ -178,6 +181,7 @@ export const buildAlertAssigneesFilter = (assigneesIds: AssigneesIdsSelection[])
|
|||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
index: 'security-solution-default',
|
||||
},
|
||||
query: combinedQuery,
|
||||
},
|
||||
|
|
|
@ -16,6 +16,7 @@ import { VIEW_SELECTION } from '../../../../common/constants';
|
|||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useGetFieldSpec } from '../../../common/hooks/use_get_field_spec';
|
||||
import { useDataViewId } from '../../../common/hooks/use_data_view_id';
|
||||
|
||||
export const getUseCellActionsHook = (tableId: TableId) => {
|
||||
const useCellActions: AlertsTableConfigurationRegistry['useCellActions'] = ({
|
||||
|
@ -24,6 +25,7 @@ export const getUseCellActionsHook = (tableId: TableId) => {
|
|||
dataGridRef,
|
||||
}) => {
|
||||
const getFieldSpec = useGetFieldSpec(SourcererScopeName.detections);
|
||||
const dataViewId = useDataViewId(SourcererScopeName.detections);
|
||||
/**
|
||||
* There is difference between how `triggers actions` fetched data v/s
|
||||
* how security solution fetches data via timelineSearchStrategy
|
||||
|
@ -58,7 +60,7 @@ export const getUseCellActionsHook = (tableId: TableId) => {
|
|||
useShallowEqualSelector((state) => (getTable(state, tableId) ?? tableDefaults).viewMode) ??
|
||||
tableDefaults.viewMode;
|
||||
|
||||
const cellActionsMetadata = useMemo(() => ({ scopeId: tableId }), []);
|
||||
const cellActionsMetadata = useMemo(() => ({ scopeId: tableId, dataViewId }), [dataViewId]);
|
||||
|
||||
const cellActionsFields = useMemo<UseDataGridColumnsSecurityCellActionsProps['fields']>(() => {
|
||||
if (viewMode === VIEW_SELECTION.eventRenderedView) {
|
||||
|
|
|
@ -160,6 +160,7 @@ jest.mock('../../../timelines/components/side_panel/hooks/use_detail_panel', ()
|
|||
}),
|
||||
};
|
||||
});
|
||||
const dataViewId = 'security-solution-default';
|
||||
|
||||
const stateWithBuildingBlockAlertsEnabled: State = {
|
||||
...mockGlobalState,
|
||||
|
@ -278,6 +279,7 @@ describe('DetectionEnginePageComponent', () => {
|
|||
type: 'exists',
|
||||
key: 'kibana.alert.building_block_type',
|
||||
value: 'exists',
|
||||
index: dataViewId,
|
||||
},
|
||||
query: {
|
||||
exists: {
|
||||
|
@ -316,6 +318,7 @@ describe('DetectionEnginePageComponent', () => {
|
|||
type: 'exists',
|
||||
key: 'kibana.alert.building_block_type',
|
||||
value: 'exists',
|
||||
index: dataViewId,
|
||||
},
|
||||
query: {
|
||||
exists: {
|
||||
|
|
|
@ -30,7 +30,7 @@ export const KubernetesContainer = React.memo(() => {
|
|||
const { kubernetesSecurity, uiSettings } = useKibana().services;
|
||||
|
||||
const { globalFullScreen } = useGlobalFullScreen();
|
||||
const { indexPattern, sourcererDataView } = useSourcererDataView();
|
||||
const { indexPattern, sourcererDataView, dataViewId } = useSourcererDataView();
|
||||
const { from, to } = useGlobalTime();
|
||||
|
||||
const getGlobalFiltersQuerySelector = useMemo(
|
||||
|
@ -91,6 +91,7 @@ export const KubernetesContainer = React.memo(() => {
|
|||
endDate: to,
|
||||
},
|
||||
renderSessionsView,
|
||||
dataViewId: dataViewId ?? undefined,
|
||||
})}
|
||||
<SpyRoute pageName={SecurityPageName.kubernetes} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
|
|
|
@ -34,10 +34,13 @@ const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
|
|||
size,
|
||||
showTooltip = false,
|
||||
value,
|
||||
dataViewId,
|
||||
}) => {
|
||||
const filterForValueFn = useCallback(() => {
|
||||
const makeFilter = (currentVal: string | null | undefined) =>
|
||||
currentVal?.length === 0 ? createFilter(field, undefined) : createFilter(field, currentVal);
|
||||
currentVal?.length === 0
|
||||
? createFilter(field, undefined, false, dataViewId)
|
||||
: createFilter(field, currentVal, false, dataViewId);
|
||||
const filters = Array.isArray(value)
|
||||
? value.map((currentVal: string | null | undefined) => makeFilter(currentVal))
|
||||
: makeFilter(value);
|
||||
|
@ -54,7 +57,7 @@ const FilterForValueButton: React.FC<FilterForValueProps> = React.memo(
|
|||
if (onClick != null) {
|
||||
onClick();
|
||||
}
|
||||
}, [field, filterManager, onClick, onFilterAdded, value]);
|
||||
}, [dataViewId, field, filterManager, onClick, onFilterAdded, value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ownFocus) {
|
||||
|
|
|
@ -33,12 +33,13 @@ const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnAr
|
|||
size,
|
||||
showTooltip = false,
|
||||
value,
|
||||
dataViewId,
|
||||
}) => {
|
||||
const filterOutValueFn = useCallback(() => {
|
||||
const makeFilter = (currentVal: string | null | undefined) =>
|
||||
currentVal == null || currentVal?.length === 0
|
||||
? createFilter(field, null, false)
|
||||
: createFilter(field, currentVal, true);
|
||||
? createFilter(field, null, false, dataViewId)
|
||||
: createFilter(field, currentVal, true, dataViewId);
|
||||
const filters = Array.isArray(value)
|
||||
? value.map((currentVal: string | null | undefined) => makeFilter(currentVal))
|
||||
: makeFilter(value);
|
||||
|
@ -54,7 +55,7 @@ const FilterOutValueButton: React.FC<HoverActionComponentProps & FilterValueFnAr
|
|||
if (onClick != null) {
|
||||
onClick();
|
||||
}
|
||||
}, [field, filterManager, onClick, onFilterAdded, value]);
|
||||
}, [field, filterManager, onClick, onFilterAdded, value, dataViewId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ownFocus) {
|
||||
|
|
|
@ -15,6 +15,7 @@ export interface FilterValueFnArgs {
|
|||
value: string[] | string | null | undefined;
|
||||
filterManager: FilterManager | undefined;
|
||||
onFilterAdded: (() => void) | undefined;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
export interface HoverActionComponentProps {
|
||||
|
|
|
@ -24,7 +24,8 @@ export const getAdditionalScreenReaderOnlyContext = ({
|
|||
export const createFilter = (
|
||||
key: string,
|
||||
value: string[] | string | null | undefined,
|
||||
negate: boolean = false
|
||||
negate: boolean = false,
|
||||
index?: string
|
||||
): Filter => {
|
||||
const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null;
|
||||
return queryValue != null
|
||||
|
@ -39,6 +40,7 @@ export const createFilter = (
|
|||
params: {
|
||||
query: queryValue,
|
||||
},
|
||||
index,
|
||||
},
|
||||
query: {
|
||||
match: {
|
||||
|
@ -60,6 +62,7 @@ export const createFilter = (
|
|||
negate: value === undefined,
|
||||
type: 'exists',
|
||||
value: 'exists',
|
||||
index,
|
||||
},
|
||||
} as Filter);
|
||||
};
|
||||
|
|
|
@ -34433,7 +34433,6 @@
|
|||
"xpack.securitySolution.exceptionsTable.manageRulesErrorDescription": "Une erreur s'est produite lors de l'association ou de la dissociation des règles",
|
||||
"xpack.securitySolution.exceptionsTable.rulesCountLabel": "Règles",
|
||||
"xpack.securitySolution.exitFullScreenButton": "Quitter le plein écran",
|
||||
"xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "Masquer les valeurs les plus élevées",
|
||||
"xpack.securitySolution.expandedValue.links.expandIpDetails": "Développer les détails d'IP",
|
||||
"xpack.securitySolution.expandedValue.links.viewEventReference": "Afficher la référence de l'événement",
|
||||
"xpack.securitySolution.expandedValue.links.viewHostSummary": "Afficher le résumé de l'hôte",
|
||||
|
@ -34442,7 +34441,6 @@
|
|||
"xpack.securitySolution.expandedValue.links.viewRuleDetails": "Afficher les détails de la règle",
|
||||
"xpack.securitySolution.expandedValue.links.viewRuleReference": "Afficher la référence de la règle",
|
||||
"xpack.securitySolution.expandedValue.links.viewUserSummary": "Afficher le résumé de l'utilisateur",
|
||||
"xpack.securitySolution.expandedValue.showTopN.showTopValues": "Afficher les valeurs les plus élevées",
|
||||
"xpack.securitySolution.explore.landing.pageTitle": "Explorer",
|
||||
"xpack.securitySolution.featureCatalogueDescription": "Prévenez, collectez, détectez et traitez les menaces pour une protection unifiée dans toute votre infrastructure.",
|
||||
"xpack.securitySolution.fieldBrowser.actionsLabel": "Actions",
|
||||
|
|
|
@ -34433,7 +34433,6 @@
|
|||
"xpack.securitySolution.exceptionsTable.manageRulesErrorDescription": "ルールのリンクまたはリンク解除中にエラーが発生しました",
|
||||
"xpack.securitySolution.exceptionsTable.rulesCountLabel": "ルール",
|
||||
"xpack.securitySolution.exitFullScreenButton": "全画面を終了",
|
||||
"xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "上位の値を非表示",
|
||||
"xpack.securitySolution.expandedValue.links.expandIpDetails": "IP詳細を展開",
|
||||
"xpack.securitySolution.expandedValue.links.viewEventReference": "イベント参照を表示",
|
||||
"xpack.securitySolution.expandedValue.links.viewHostSummary": "ホスト概要を表示",
|
||||
|
@ -34442,7 +34441,6 @@
|
|||
"xpack.securitySolution.expandedValue.links.viewRuleDetails": "ルール詳細を表示",
|
||||
"xpack.securitySolution.expandedValue.links.viewRuleReference": "ルール参照を表示",
|
||||
"xpack.securitySolution.expandedValue.links.viewUserSummary": "ユーザー概要を表示",
|
||||
"xpack.securitySolution.expandedValue.showTopN.showTopValues": "上位の値を表示",
|
||||
"xpack.securitySolution.explore.landing.pageTitle": "探索",
|
||||
"xpack.securitySolution.featureCatalogueDescription": "インフラストラクチャー全体の統合保護のため、脅威を防止、収集、検出し、それに対応します。",
|
||||
"xpack.securitySolution.fieldBrowser.actionsLabel": "アクション",
|
||||
|
|
|
@ -34415,7 +34415,6 @@
|
|||
"xpack.securitySolution.exceptionsTable.manageRulesErrorDescription": "链接或取消链接规则时出错",
|
||||
"xpack.securitySolution.exceptionsTable.rulesCountLabel": "规则",
|
||||
"xpack.securitySolution.exitFullScreenButton": "退出全屏",
|
||||
"xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "隐藏排名最前值",
|
||||
"xpack.securitySolution.expandedValue.links.expandIpDetails": "展开 IP 详情",
|
||||
"xpack.securitySolution.expandedValue.links.viewEventReference": "查看事件参考",
|
||||
"xpack.securitySolution.expandedValue.links.viewHostSummary": "查看主机摘要",
|
||||
|
@ -34424,7 +34423,6 @@
|
|||
"xpack.securitySolution.expandedValue.links.viewRuleDetails": "查看规则详情",
|
||||
"xpack.securitySolution.expandedValue.links.viewRuleReference": "查看规则参考",
|
||||
"xpack.securitySolution.expandedValue.links.viewUserSummary": "查看用户摘要",
|
||||
"xpack.securitySolution.expandedValue.showTopN.showTopValues": "显示排名最前值",
|
||||
"xpack.securitySolution.explore.landing.pageTitle": "浏览",
|
||||
"xpack.securitySolution.featureCatalogueDescription": "预防、收集、检测和响应威胁,以对整个基础架构提供统一的保护。",
|
||||
"xpack.securitySolution.fieldBrowser.actionsLabel": "操作",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue