[Security Solution][Alert details] - move table tab content to flyout folder (#189140)

This commit is contained in:
Philippe Oberti 2024-07-31 22:26:43 +02:00 committed by GitHub
parent bdc9a6c98e
commit deb69fb948
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 474 additions and 1481 deletions

View file

@ -1,93 +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 { ReactWrapper } from 'enzyme';
import React from 'react';
import { getColumns } from './columns';
import { TestProviders } from '../../mock';
import { useMountAppended } from '../../utils/use_mount_appended';
import { mockBrowserFields } from '../../containers/source/mock';
import type { EventFieldsData } from './types';
jest.mock('../../lib/kibana');
jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => {
const actual = jest.requireActual('@kbn/cell-actions/src/hooks/use_load_actions');
return {
...actual,
useLoadActions: jest.fn().mockImplementation(() => ({
value: [],
error: undefined,
loading: false,
})),
};
});
jest.mock('../../hooks/use_get_field_spec');
interface Column {
field: string;
name: string | JSX.Element;
sortable: boolean;
render: (field: string, data: EventFieldsData) => JSX.Element;
}
describe('getColumns', () => {
const mount = useMountAppended();
const defaultProps = {
browserFields: mockBrowserFields,
columnHeaders: [],
contextId: 'some-context',
eventId: 'some-event',
getLinkValue: jest.fn(),
onUpdateColumns: jest.fn(),
scopeId: 'some-timeline',
toggleColumn: jest.fn(),
};
test('should have expected fields', () => {
const columns = getColumns(defaultProps);
columns.forEach((column) => {
expect(column).toHaveProperty('field');
expect(column).toHaveProperty('name');
expect(column).toHaveProperty('render');
expect(column).toHaveProperty('sortable');
});
});
describe('column actions', () => {
let actionsColumn: Column;
const mockDataToUse = mockBrowserFields.agent.fields;
const testValue = 'testValue';
const testData = {
type: 'someType',
category: 'agent',
field: 'agent.id',
...mockDataToUse,
} as EventFieldsData;
beforeEach(() => {
actionsColumn = getColumns(defaultProps)[0] as Column;
});
test('it renders inline actions', () => {
const wrapper = mount(
<TestProviders>{actionsColumn.render(testValue, testData)}</TestProviders>
) as ReactWrapper;
expect(wrapper.find('[data-test-subj="inlineActions"]').exists()).toBeTruthy();
});
test('it does not render inline actions when readOnly prop is passed', () => {
actionsColumn = getColumns({ ...defaultProps, isReadOnly: true })[0] as Column;
const wrapper = mount(
<TestProviders>{actionsColumn.render(testValue, testData)}</TestProviders>
) as ReactWrapper;
expect(wrapper.find('[data-test-subj="inlineActions"]').exists()).toBeFalsy();
});
});
});

View file

@ -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 { EuiPanel, EuiText } from '@elastic/eui';
import memoizeOne from 'memoize-one';
import React from 'react';
import styled from 'styled-components';
import { getCategory } from '@kbn/triggers-actions-ui-plugin/public';
import { SecurityCellActions, CellActionsMode, SecurityCellActionsTrigger } from '../cell_actions';
import type { BrowserFields } from '../../containers/source';
import * as i18n from './translations';
import type { EventFieldsData } from './types';
import type { BrowserField } from '../../../../common/search_strategy';
import { FieldValueCell } from './table/field_value_cell';
import { FieldNameCell } from './table/field_name_cell';
import { getSourcererScopeId } from '../../../helpers';
import type { ColumnsProvider } from './event_fields_browser';
const HoverActionsContainer = styled(EuiPanel)`
align-items: center;
display: flex;
flex-direction: row;
height: 25px;
justify-content: center;
left: 5px;
position: absolute;
top: -10px;
width: 30px;
`;
HoverActionsContainer.displayName = 'HoverActionsContainer';
export const getFieldFromBrowserField = memoizeOne(
(field: string, browserFields: BrowserFields): BrowserField | undefined => {
const category = getCategory(field);
return browserFields[category]?.fields?.[field] as BrowserField;
},
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
);
export const getColumns: ColumnsProvider = ({
browserFields,
eventId,
contextId,
scopeId,
getLinkValue,
isDraggable,
isReadOnly,
}) => [
...(!isReadOnly
? ([
{
field: 'values',
name: (
<EuiText size="xs">
<strong>{i18n.ACTIONS}</strong>
</EuiText>
),
sortable: false,
truncateText: false,
width: '132px',
render: (values, data) => {
return (
<SecurityCellActions
data={{
field: data.field,
value: values,
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.INLINE}
visibleCellActions={3}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId, isObjectArray: data.isObjectArray }}
/>
);
},
},
] as ReturnType<ColumnsProvider>)
: []),
{
field: 'field',
className: 'eventFieldsTable__fieldNameCell',
name: (
<EuiText size="xs">
<strong>{i18n.FIELD}</strong>
</EuiText>
),
sortable: true,
truncateText: false,
render: (field, data) => {
return (
<FieldNameCell data={data as EventFieldsData} field={field} fieldMapping={undefined} />
);
},
},
{
field: 'values',
className: 'eventFieldsTable__fieldValueCell',
name: (
<EuiText size="xs">
<strong>{i18n.VALUE}</strong>
</EuiText>
),
sortable: true,
truncateText: false,
render: (values, data) => {
const fieldFromBrowserField = getFieldFromBrowserField(data.field, browserFields);
return (
<FieldValueCell
contextId={contextId}
data={data as EventFieldsData}
eventId={eventId}
fieldFromBrowserField={fieldFromBrowserField}
getLinkValue={getLinkValue}
isDraggable={isDraggable}
values={values}
/>
);
},
},
];

View file

@ -1,210 +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 { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item';
import { TestProviders } from '../../mock/test_providers';
import { EventFieldsBrowser } from './event_fields_browser';
import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
import { TimelineTabs } from '../../../../common/types/timeline';
jest.mock('../../lib/kibana');
jest.mock('../../hooks/use_get_field_spec');
jest.mock('@kbn/cell-actions/src/hooks/use_load_actions', () => {
const actual = jest.requireActual('@kbn/cell-actions/src/hooks/use_load_actions');
return {
...actual,
useLoadActions: jest.fn().mockImplementation(() => ({
value: [],
error: undefined,
loading: false,
})),
};
});
jest.mock('../link_to');
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
describe('EventFieldsBrowser', () => {
const mount = useMountAppended();
describe('column headers', () => {
['Actions', 'Field', 'Value'].forEach((header) => {
test(`it renders the ${header} column header`, () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(wrapper.find('thead').contains(header)).toBeTruthy();
});
});
});
describe('filter input', () => {
test('it renders a filter input with the expected placeholder', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(wrapper.find('input[type="search"]').props().placeholder).toEqual(
'Filter by Field, Value, or Description...'
);
});
});
describe('Hover Actions', () => {
const eventId = 'pEMaMmkBUV60JmNWmWVi';
test('it renders inline actions', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={eventId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="inlineActions"]').exists()).toBeTruthy();
});
});
describe('field type icon', () => {
test('it renders the expected icon type for the data provided', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(
wrapper
.find('tr.euiTableRow')
.find('td.euiTableRowCell')
.at(1)
.find('[data-euiicon-type]')
.exists()
).toEqual(true);
});
});
describe('field', () => {
test('it renders the field name for the data provided', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="field-name"]').at(0).text()).toEqual('@timestamp');
});
test('it renders the expected icon for description', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(
wrapper
.find('tr.euiTableRow')
.find('td.euiTableRowCell')
.at(1)
.find('[data-euiicon-type]')
.last()
.prop('data-euiicon-type')
).toEqual('tokenDate');
});
});
describe('value', () => {
test('it renders the expected value for the data provided', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="localized-date-tool-tip"]').at(0).text()).toEqual(
'Feb 28, 2019 @ 16:50:54.621'
);
});
});
describe('description', () => {
test('it renders the expected field description the data provided', () => {
const wrapper = mount(
<TestProviders>
<EventFieldsBrowser
browserFields={mockBrowserFields}
data={mockDetailItemData}
eventId={mockDetailItemDataId}
scopeId="timeline-test"
timelineTabType={TimelineTabs.query}
/>
</TestProviders>
);
expect(
wrapper.find('[data-test-subj="field-name-cell"]').at(0).find('EuiToolTip').prop('content')
).toContain('Date/time when the event originated.');
});
});
});

View file

@ -1,308 +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 { getOr, noop, sortBy } from 'lodash/fp';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { EuiInMemoryTable } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { rgba } from 'polished';
import styled from 'styled-components';
import {
arrayIndexToAriaIndex,
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
isTab,
onKeyDownFocusHandler,
} from '@kbn/timelines-plugin/public';
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
import { isInTableScope, isTimelineScope } from '../../../helpers';
import { timelineSelectors } from '../../../timelines/store';
import type { BrowserFields } from '../../containers/source';
import { getAllFieldsByName } from '../../containers/source';
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers';
import { timelineDefaults } from '../../../timelines/store/defaults';
import { getColumns } from './columns';
import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import type { TimelineTabs } from '../../../../common/types/timeline';
export type ColumnsProvider = (providerOptions: {
browserFields: BrowserFields;
eventId: string;
contextId: string;
scopeId: string;
getLinkValue: (field: string) => string | null;
isDraggable?: boolean;
isReadOnly?: boolean;
}) => Array<EuiBasicTableColumn<TimelineEventsDetailsItem>>;
interface Props {
browserFields: BrowserFields;
data: TimelineEventsDetailsItem[];
eventId: string;
isDraggable?: boolean;
scopeId: string;
timelineTabType: TimelineTabs | 'flyout';
isReadOnly?: boolean;
columnsProvider?: ColumnsProvider;
}
const TableWrapper = styled.div`
display: flex;
flex: 1;
overflow: hidden;
> div {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
> .euiFlexGroup:first-of-type {
flex: 0;
}
}
`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)`
flex: 1;
overflow: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
height: ${({ theme }) => theme.eui.euiScrollBar};
width: ${({ theme }) => theme.eui.euiScrollBar};
}
&::-webkit-scrollbar-thumb {
background-clip: content-box;
background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
}
&::-webkit-scrollbar-corner,
&::-webkit-scrollbar-track {
background-color: transparent;
}
.eventFieldsTable__fieldIcon {
padding-top: ${({ theme }) => parseFloat(theme.eui.euiSizeXS) * 1.5}px;
}
.eventFieldsTable__fieldName {
line-height: ${({ theme }) => theme.eui.euiLineHeight};
padding: ${({ theme }) => theme.eui.euiSizeXS};
}
// TODO: Use this logic from discover
/* .eventFieldsTable__multiFieldBadge {
font: ${({ theme }) => theme.eui.euiFont};
} */
.inlineActions {
opacity: 0;
}
.eventFieldsTable__tableRow {
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
font-family: ${({ theme }) => theme.eui.euiCodeFontFamily};
.inlineActions-popoverOpen {
opacity: 1;
}
&:hover {
.inlineActions {
opacity: 1;
}
}
}
.eventFieldsTable__actionCell,
.eventFieldsTable__fieldNameCell {
align-items: flex-start;
padding: ${({ theme }) => theme.eui.euiSizeXS};
}
.eventFieldsTable__fieldValue {
display: inline-block;
word-break: break-all;
word-wrap: break-word;
white-space: pre-wrap;
line-height: ${({ theme }) => theme.eui.euiLineHeight};
color: ${({ theme }) => theme.eui.euiColorFullShade};
vertical-align: top;
}
`;
// Match structure in discover
const COUNT_PER_PAGE_OPTIONS = [25, 50, 100];
// Encapsulating the pagination logic for the table.
const useFieldBrowserPagination = () => {
const [pagination, setPagination] = useState<{ pageIndex: number }>({
pageIndex: 0,
});
const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => {
setPagination({ pageIndex: index });
}, []);
const paginationTableProp = useMemo(
() => ({
...pagination,
pageSizeOptions: COUNT_PER_PAGE_OPTIONS,
}),
[pagination]
);
return {
onTableChange,
paginationTableProp,
};
};
/**
* This callback, invoked via `EuiInMemoryTable`'s `rowProps, assigns
* attributes to every `<tr>`.
*/
/** Renders a table view or JSON view of the `ECS` `data` */
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
export const EventFieldsBrowser = React.memo<Props>(
({
browserFields,
data,
eventId,
isDraggable,
timelineTabType,
scopeId,
isReadOnly,
columnsProvider = getColumns,
}) => {
const containerElement = useRef<HTMLDivElement | null>(null);
const getScope = useMemo(() => {
if (isTimelineScope(scopeId)) {
return timelineSelectors.getTimelineByIdSelector();
} else if (isInTableScope(scopeId)) {
return dataTableSelectors.getTableByIdSelector();
}
}, [scopeId]);
const defaults = isTimelineScope(scopeId) ? timelineDefaults : tableDefaults;
const columnHeaders = useDeepEqualSelector((state) => {
const { columns } = (getScope && getScope(state, scopeId)) ?? defaults;
return getColumnHeaders(columns, browserFields);
});
const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
const items = useMemo(
() =>
sortBy(['field'], data).map((item, i) => ({
...item,
...fieldsByName[item.field],
valuesConcatenated: item.values != null ? item.values.join() : '',
ariaRowindex: arrayIndexToAriaIndex(i),
})),
[data, fieldsByName]
);
const getLinkValue = useCallback(
(field: string) => {
const linkField = (columnHeaders.find((col) => col.id === field) ?? {}).linkField;
if (!linkField) {
return null;
}
const linkFieldData = (data ?? []).find((d) => d.field === linkField);
const linkFieldValue = getOr(null, 'originalValue', linkFieldData);
return Array.isArray(linkFieldValue) ? linkFieldValue[0] : linkFieldValue;
},
[data, columnHeaders]
);
const onSetRowProps = useCallback(({ ariaRowindex, field }: TimelineEventsDetailsItem) => {
const rowIndex = ariaRowindex != null ? { 'data-rowindex': ariaRowindex } : {};
return {
...rowIndex,
className: 'eventFieldsTable__tableRow',
'data-test-subj': `event-fields-table-row-${field}`,
};
}, []);
const columns = useMemo(
() =>
columnsProvider({
browserFields,
eventId,
contextId: `event-fields-browser-for-${scopeId}-${timelineTabType}`,
scopeId,
getLinkValue,
isDraggable,
isReadOnly,
}),
[
browserFields,
eventId,
scopeId,
columnsProvider,
timelineTabType,
getLinkValue,
isDraggable,
isReadOnly,
]
);
const focusSearchInput = useCallback(() => {
// the selector below is used to focus the input because EuiInMemoryTable does not expose a ref to its built-in search input
containerElement.current?.querySelector<HTMLInputElement>('input[type="search"]')?.focus();
}, []);
const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
if (isTab(keyboardEvent)) {
onEventDetailsTabKeyPressed({
containerElement: containerElement.current,
keyboardEvent,
onSkipFocusBeforeEventsTable: focusSearchInput,
onSkipFocusAfterEventsTable: noop,
});
} else {
onKeyDownFocusHandler({
colindexAttribute: DATA_COLINDEX_ATTRIBUTE,
containerElement: containerElement?.current,
event: keyboardEvent,
maxAriaColindex: 3,
maxAriaRowindex: data.length,
onColumnFocused: noop,
rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE,
});
}
},
[data, focusSearchInput]
);
useEffect(() => {
focusSearchInput();
}, [focusSearchInput]);
// Pagination
const { onTableChange, paginationTableProp } = useFieldBrowserPagination();
return (
<TableWrapper onKeyDown={onKeyDown} ref={containerElement}>
<StyledEuiInMemoryTable
className={EVENT_FIELDS_TABLE_CLASS_NAME}
items={items}
itemId="field"
columns={columns}
onTableChange={onTableChange}
pagination={paginationTableProp}
rowProps={onSetRowProps}
search={search}
sorting={false}
data-test-subj="event-fields-browser"
/>
</TableWrapper>
);
}
);
EventFieldsBrowser.displayName = 'EventFieldsBrowser';

View file

@ -18,25 +18,12 @@ import type { BrowserFields } from '../../containers/source';
import type { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import type { EnrichedFieldInfo, EventSummaryField } from './types';
import * as i18n from './translations';
import {
AGENT_STATUS_FIELD_NAME,
QUARANTINED_PATH_FIELD_NAME,
} from '../../../timelines/components/timeline/body/renderers/constants';
import { RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELD } from '../../../../common/endpoint/service/response_actions/constants';
/**
* Defines the behavior of the search input that appears above the table of data
*/
export const search = {
box: {
incremental: true,
placeholder: i18n.PLACEHOLDER,
schema: true,
'data-test-subj': 'search-input',
},
};
/**
* An item rendered in the table
*/

View file

@ -1,93 +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 { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, EuiToolTip } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { FieldIcon } from '@kbn/react-field';
import type { DataViewField } from '@kbn/data-views-plugin/common';
import { EcsFlat } from '@elastic/ecs';
import * as i18n from '../translations';
import { getExampleText } from '../helpers';
import type { EventFieldsData } from '../types';
import { getFieldTypeName } from './get_field_type_name';
const getEcsField = (field: string): { example?: string; description?: string } | undefined => {
return EcsFlat[field as keyof typeof EcsFlat] as
| {
example?: string;
description?: string;
}
| undefined;
};
export interface FieldNameCellProps {
data: EventFieldsData;
field: string;
fieldMapping?: DataViewField;
scripted?: boolean;
}
export const FieldNameCell = React.memo(
({ data, field, fieldMapping, scripted }: FieldNameCellProps) => {
const ecsField = getEcsField(field);
const typeName = getFieldTypeName(data.type);
// TODO: We don't have fieldMapping or isMultiField until kibana indexPatterns is implemented. Will default to field for now
const displayName = fieldMapping && fieldMapping.displayName ? fieldMapping.displayName : field;
const defaultTooltip = displayName !== field ? `${field} (${displayName})` : field;
const isMultiField = fieldMapping?.isSubtypeMulti();
return (
<>
<EuiFlexItem grow={false} className="eventFieldsTable__fieldIcon">
<FieldIcon
data-test-subj="field-type-icon"
type={data.type}
label={typeName}
scripted={scripted} // TODO: Will get with kibana indexPatterns;
/>
</EuiFlexItem>
<EuiFlexGroup
wrap={true}
gutterSize="none"
responsive={false}
alignItems="flexStart"
data-test-subj="field-name-cell"
>
<EuiFlexItem className="eventFieldsTable__fieldName eui-textBreakAll" grow={false}>
<EuiToolTip
position="top"
content={
!isEmpty(ecsField?.description)
? `${ecsField?.description} ${getExampleText(ecsField?.example)}`
: defaultTooltip
}
delay="long"
anchorClassName="eui-textBreakAll"
>
<EuiText size="xs" data-test-subj="field-name">
{field}
</EuiText>
</EuiToolTip>
</EuiFlexItem>
{isMultiField && (
<EuiToolTip position="top" delay="long" content={i18n.MULTI_FIELD_TOOLTIP}>
<EuiBadge
title=""
className="eventFieldsTable__multiFieldBadge"
color="default"
data-test-subj={`eventFieldsTableRow-${field}-multifieldBadge`}
>
{i18n.MULTI_FIELD_BADGE}
</EuiBadge>
</EuiToolTip>
)}
</EuiFlexGroup>
</>
);
}
);
FieldNameCell.displayName = 'FieldNameCell';

View file

@ -1,93 +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 { CSSProperties } from 'react';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import type { BrowserField } from '../../../containers/source';
import { OverflowField } from '../../tables/helpers';
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
import { MESSAGE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
import type { EventFieldsData, FieldsData } from '../types';
import { getFieldFormat } from '../get_field_format';
export interface FieldValueCellProps {
contextId: string;
data: EventFieldsData | FieldsData;
eventId: string;
fieldFromBrowserField?: Partial<BrowserField>;
getLinkValue?: (field: string) => string | null;
isDraggable?: boolean;
linkValue?: string | null | undefined;
style?: CSSProperties | undefined;
values: string[] | null | undefined;
}
export const FieldValueCell = React.memo(
({
contextId,
data,
eventId,
fieldFromBrowserField,
getLinkValue,
isDraggable = false,
linkValue,
style,
values,
}: FieldValueCellProps) => {
return (
<EuiFlexGroup
alignItems="flexStart"
data-test-subj={`event-field-${data.field}`}
direction="column"
gutterSize="none"
style={style}
>
{values != null &&
values.map((value, i) => {
if (fieldFromBrowserField == null) {
return (
<EuiFlexItem grow={false} key={`${i}-${value}`}>
<EuiText size="xs" key={`${i}-${value}`}>
{value}
</EuiText>
</EuiFlexItem>
);
}
return (
<EuiFlexItem
className="eventFieldsTable__fieldValue"
grow={false}
key={`${i}-${value}`}
>
{data.field === MESSAGE_FIELD_NAME ? (
<OverflowField value={value} />
) : (
<FormattedFieldValue
contextId={`${contextId}-${eventId}-${data.field}-${i}-${value}`}
eventId={eventId}
fieldFormat={getFieldFormat(data)}
fieldName={data.field}
fieldFromBrowserField={fieldFromBrowserField}
fieldType={data.type}
isAggregatable={fieldFromBrowserField.aggregatable}
isDraggable={isDraggable}
isObjectArray={data.isObjectArray}
value={value}
linkValue={(getLinkValue && getLinkValue(data.field)) ?? linkValue}
truncate={false}
/>
)}
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
}
);
FieldValueCell.displayName = 'FieldValueCell';

View file

@ -1,62 +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 { i18n } from '@kbn/i18n';
export function getFieldTypeName(type: string) {
switch (type) {
case 'boolean':
return i18n.translate('xpack.securitySolution.fieldNameIcons.booleanAriaLabel', {
defaultMessage: 'Boolean field',
});
case 'conflict':
return i18n.translate('xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel', {
defaultMessage: 'Conflicting field',
});
case 'date':
return i18n.translate('xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel', {
defaultMessage: 'Date field',
});
case 'geo_point':
return i18n.translate('xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel', {
defaultMessage: 'Geo point field',
});
case 'geo_shape':
return i18n.translate('xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel', {
defaultMessage: 'Geo shape field',
});
case 'ip':
return i18n.translate('xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel', {
defaultMessage: 'IP address field',
});
case 'murmur3':
return i18n.translate('xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel', {
defaultMessage: 'Murmur3 field',
});
case 'number':
return i18n.translate('xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel', {
defaultMessage: 'Number field',
});
case 'source':
// Note that this type is currently not provided, type for _source is undefined
return i18n.translate('xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel', {
defaultMessage: 'Source field',
});
case 'string':
return i18n.translate('xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel', {
defaultMessage: 'String field',
});
case 'nested':
return i18n.translate('xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel', {
defaultMessage: 'Nested field',
});
default:
return i18n.translate('xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel', {
defaultMessage: 'Unknown field',
});
}
}

View file

@ -1,104 +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 { render, screen } from '@testing-library/react';
import React from 'react';
import type { BrowserField } from '../../../containers/source';
import { PrevalenceCellRenderer } from './prevalence_cell';
import { TestProviders } from '../../../mock';
import type { EventFieldsData } from '../types';
import { TimelineId } from '../../../../../common/types';
import type { AlertSummaryRow } from '../helpers';
import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence';
import { getEmptyValue } from '../../empty_value';
jest.mock('../../../lib/kibana');
jest.mock('../../../containers/alerts/use_alert_prevalence', () => ({
useAlertPrevalence: jest.fn(),
}));
const mockUseAlertPrevalence = useAlertPrevalence as jest.Mock;
const eventId = 'TUWyf3wBFCFU0qRJTauW';
const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'];
const hostIpFieldFromBrowserField: BrowserField = {
aggregatable: true,
name: 'host.ip',
readFromDocValues: false,
searchable: true,
type: 'ip',
};
const hostIpData: EventFieldsData = {
...hostIpFieldFromBrowserField,
ariaRowindex: 35,
field: 'host.ip',
isObjectArray: false,
originalValue: [...hostIpValues],
values: [...hostIpValues],
};
const enrichedHostIpData: AlertSummaryRow['description'] = {
data: { ...hostIpData },
eventId,
fieldFromBrowserField: { ...hostIpFieldFromBrowserField },
isDraggable: false,
scopeId: TimelineId.test,
values: [...hostIpValues],
};
describe('PrevalenceCellRenderer', () => {
describe('When data is loading', () => {
test('it should show the loading spinner', async () => {
mockUseAlertPrevalence.mockImplementation(() => ({
loading: true,
count: 123,
error: true,
}));
const { container } = render(
<TestProviders>
<PrevalenceCellRenderer {...enrichedHostIpData} />
</TestProviders>
);
expect(container.getElementsByClassName('euiLoadingSpinner')).toHaveLength(1);
});
});
describe('When an error was returned', () => {
test('it should return empty value placeholder', async () => {
mockUseAlertPrevalence.mockImplementation(() => ({
loading: false,
count: undefined,
error: true,
}));
const { container } = render(
<TestProviders>
<PrevalenceCellRenderer {...enrichedHostIpData} />
</TestProviders>
);
expect(container.getElementsByClassName('euiLoadingSpinner')).toHaveLength(0);
expect(screen.queryByText('123')).toBeNull();
expect(screen.queryByText(getEmptyValue())).toBeTruthy();
});
});
describe('When an actual count is returned', () => {
test('it should show the count', async () => {
mockUseAlertPrevalence.mockImplementation(() => ({
loading: false,
count: 123,
error: false,
}));
const { container } = render(
<TestProviders>
<PrevalenceCellRenderer {...enrichedHostIpData} />
</TestProviders>
);
expect(container.getElementsByClassName('euiLoadingSpinner')).toHaveLength(0);
expect(screen.queryByText('123')).toBeInTheDocument();
});
});
});

View file

@ -1,74 +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 { EuiLoadingSpinner } from '@elastic/eui';
import { TimelineId } from '../../../../../common/types';
import type { AlertSummaryRow } from '../helpers';
import { getEmptyTagValue } from '../../empty_value';
import { InvestigateInTimelineButton } from './investigate_in_timeline_button';
import { useActionCellDataProvider } from './use_action_cell_data_provider';
import { useAlertPrevalence } from '../../../containers/alerts/use_alert_prevalence';
import { getFieldFormat } from '../get_field_format';
/**
* Renders a Prevalence cell based on a regular alert prevalence query
*/
const PrevalenceCell: React.FC<AlertSummaryRow['description']> = ({
data,
eventId,
fieldFromBrowserField,
linkValue,
scopeId,
values,
}) => {
const { loading, count } = useAlertPrevalence({
field: data.field,
isActiveTimelines: scopeId === TimelineId.active,
value: values,
signalIndexName: null,
});
const cellDataProviders = useActionCellDataProvider({
contextId: scopeId,
eventId,
field: data.field,
fieldFormat: getFieldFormat(data),
fieldFromBrowserField,
fieldType: data.type,
isObjectArray: data.isObjectArray,
linkValue,
values,
});
if (loading) {
return <EuiLoadingSpinner />;
} else if (
typeof count === 'number' &&
cellDataProviders?.dataProviders &&
cellDataProviders?.dataProviders.length
) {
return (
<InvestigateInTimelineButton
asEmptyButton={true}
dataProviders={cellDataProviders.dataProviders}
filters={cellDataProviders.filters}
>
<span data-test-subj="alert-prevalence">{count}</span>
</InvestigateInTimelineButton>
);
} else {
return getEmptyTagValue();
}
};
PrevalenceCell.displayName = 'PrevalenceCell';
export const PrevalenceCellRenderer = (data: AlertSummaryRow['description']) => (
<PrevalenceCell {...data} />
);

View file

@ -1,107 +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 { act, render, screen } from '@testing-library/react';
import React from 'react';
import type { BrowserField } from '../../../containers/source';
import { SummaryValueCell } from './summary_value_cell';
import { TestProviders } from '../../../mock';
import type { EventFieldsData } from '../types';
import type { AlertSummaryRow } from '../helpers';
import { TimelineId } from '../../../../../common/types';
import { AGENT_STATUS_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
jest.mock('../../../lib/kibana');
jest.mock('../../../hooks/use_get_field_spec');
const eventId = 'TUWyf3wBFCFU0qRJTauW';
const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'];
const hostIpFieldFromBrowserField: BrowserField = {
aggregatable: true,
name: 'host.ip',
readFromDocValues: false,
searchable: true,
type: 'ip',
};
const hostIpData: EventFieldsData = {
...hostIpFieldFromBrowserField,
ariaRowindex: 35,
field: 'host.ip',
isObjectArray: false,
originalValue: [...hostIpValues],
values: [...hostIpValues],
};
const enrichedHostIpData: AlertSummaryRow['description'] = {
data: { ...hostIpData },
eventId,
fieldFromBrowserField: { ...hostIpFieldFromBrowserField },
isDraggable: false,
scopeId: TimelineId.test,
values: [...hostIpValues],
};
const enrichedAgentStatusData: AlertSummaryRow['description'] = {
data: {
field: AGENT_STATUS_FIELD_NAME,
format: '',
type: '',
aggregatable: false,
name: AGENT_STATUS_FIELD_NAME,
searchable: false,
readFromDocValues: false,
isObjectArray: false,
},
eventId,
values: [],
scopeId: TimelineId.test,
};
describe('SummaryValueCell', () => {
test('it should render', async () => {
await act(async () => {
render(
<TestProviders>
<SummaryValueCell {...enrichedHostIpData} />
</TestProviders>
);
});
hostIpValues.forEach((ipValue) => expect(screen.getByText(ipValue)).toBeInTheDocument());
expect(screen.getByTestId('inlineActions')).toBeInTheDocument();
});
describe('Without hover actions', () => {
test('When in the timeline flyout with timelineId active', async () => {
await act(async () => {
render(
<TestProviders>
<SummaryValueCell {...enrichedHostIpData} scopeId={TimelineId.active} />
</TestProviders>
);
});
hostIpValues.forEach((ipValue) => expect(screen.getByText(ipValue)).toBeInTheDocument());
expect(screen.queryByTestId('inlineActions')).not.toBeInTheDocument();
});
test('When rendering the host status field', async () => {
await act(async () => {
render(
<TestProviders>
<SummaryValueCell {...enrichedAgentStatusData} />
</TestProviders>
);
});
expect(screen.getByTestId('event-field-agent.status')).toBeInTheDocument();
expect(screen.queryByTestId('inlineActions')).not.toBeInTheDocument();
});
});
});

View file

@ -1,64 +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 {
SecurityCellActions,
CellActionsMode,
SecurityCellActionsTrigger,
} from '../../cell_actions';
import { FieldValueCell } from './field_value_cell';
import type { AlertSummaryRow } from '../helpers';
import { hasHoverOrRowActions } from '../helpers';
import { TimelineId } from '../../../../../common/types';
import { getSourcererScopeId } from '../../../../helpers';
const style = { flexGrow: 0 };
export const SummaryValueCell: React.FC<AlertSummaryRow['description']> = ({
data,
eventId,
fieldFromBrowserField,
isDraggable,
linkValue,
scopeId,
values,
isReadOnly,
}) => {
const hoverActionsEnabled = hasHoverOrRowActions(data.field);
return (
<>
<FieldValueCell
contextId={scopeId}
data={data}
eventId={eventId}
fieldFromBrowserField={fieldFromBrowserField}
linkValue={linkValue}
isDraggable={isDraggable}
style={style}
values={values}
/>
{scopeId !== TimelineId.active && !isReadOnly && hoverActionsEnabled && (
<SecurityCellActions
data={{
field: data.field,
value: values,
}}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
mode={CellActionsMode.INLINE}
visibleCellActions={3}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
/>
)}
</>
);
};
SummaryValueCell.displayName = 'SummaryValueCell';

View file

@ -29,25 +29,10 @@ export const RESPONSE_ACTIONS_VIEW = i18n.translate(
}
);
export const FIELD = i18n.translate('xpack.securitySolution.eventDetails.field', {
defaultMessage: 'Field',
});
export const VALUE = i18n.translate('xpack.securitySolution.eventDetails.value', {
defaultMessage: 'Value',
});
export const DESCRIPTION = i18n.translate('xpack.securitySolution.eventDetails.description', {
defaultMessage: 'Description',
});
export const PLACEHOLDER = i18n.translate(
'xpack.securitySolution.eventDetails.filter.placeholder',
{
defaultMessage: 'Filter by Field, Value, or Description...',
}
);
export const AGENT_STATUS = i18n.translate('xpack.securitySolution.detections.alerts.agentStatus', {
defaultMessage: 'Agent status',
});

View file

@ -0,0 +1,42 @@
/*
* 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 { render } from '@testing-library/react';
import React from 'react';
import { TableFieldNameCell } from './table_field_name_cell';
import {
FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID,
FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID,
} from './test_ids';
const mockDataType = 'date';
const mockField = '@timestamp';
describe('TableFieldNameCell', () => {
it('should render icon and text', () => {
const { getByTestId } = render(
<TableFieldNameCell dataType={mockDataType} field={mockField} />
);
expect(getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID)).toBeInTheDocument();
expect(
getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID).querySelector('span')
).toHaveAttribute('data-euiicon-type', 'tokenDate');
expect(getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID)).toHaveTextContent(mockField);
});
it('should render default icon', () => {
const { getByTestId } = render(
<TableFieldNameCell dataType={'wrong_type'} field={mockField} />
);
expect(
getByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID).querySelector('span')
).toHaveAttribute('data-euiicon-type', 'questionInCircle');
});
});

View file

@ -0,0 +1,81 @@
/*
* 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, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { FieldIcon } from '@kbn/react-field';
import { EcsFlat } from '@elastic/ecs';
import { getFieldTypeName } from '@kbn/field-utils';
import {
FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID,
FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID,
} from './test_ids';
import { getExampleText } from '../../../../common/components/event_details/helpers';
const getEcsField = (field: string): { example?: string; description?: string } | undefined => {
return EcsFlat[field as keyof typeof EcsFlat] as
| {
example?: string;
description?: string;
}
| undefined;
};
export interface TableFieldNameCellProps {
/**
* Type used to pick the correct icon
*/
dataType: string;
/**
* Field name
*/
field: string;
}
/**
* Renders an icon/text couple in the first column of the table
*/
export const TableFieldNameCell = memo(({ dataType, field }: TableFieldNameCellProps) => {
const ecsField = getEcsField(field);
const typeName = getFieldTypeName(dataType);
return (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<FieldIcon
data-test-subj={FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID}
type={dataType}
label={typeName}
scripted={undefined}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup wrap={true} gutterSize="none" responsive={false}>
<EuiFlexItem className="eui-textBreakAll" grow={false}>
<EuiToolTip
position="top"
content={
!isEmpty(ecsField?.description)
? `${ecsField?.description} ${getExampleText(ecsField?.example)}`
: field
}
delay="long"
anchorClassName="eui-textBreakAll"
>
<EuiText size="xs" data-test-subj={FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID}>
{field}
</EuiText>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
});
TableFieldNameCell.displayName = 'TableFieldNameCell';

View file

@ -8,10 +8,10 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import type { BrowserField } from '../../../containers/source';
import { FieldValueCell } from './field_value_cell';
import { TestProviders } from '../../../mock';
import type { EventFieldsData } from '../types';
import type { BrowserField } from '@kbn/timelines-plugin/common';
import type { EventFieldsData } from '../../../../common/components/event_details/types';
import { TableFieldValueCell } from './table_field_value_cell';
import { TestProviders } from '../../../../common/mock';
const contextId = 'test';
@ -31,12 +31,12 @@ const hostIpData: EventFieldsData = {
};
const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', 'fe80::4001:aff:fec8:32'];
describe('FieldValueCell', () => {
describe('TableFieldValueCell', () => {
describe('common behavior', () => {
beforeEach(() => {
render(
<TestProviders>
<FieldValueCell
<TableFieldValueCell
contextId={contextId}
data={hostIpData}
eventId={eventId}
@ -46,7 +46,7 @@ describe('FieldValueCell', () => {
);
});
test('it formats multiple values such that each value is displayed on a single line', () => {
it('should format multiple values such that each value is displayed on a single line', () => {
expect(screen.getByTestId(`event-field-${hostIpData.field}`).className).toContain('column');
});
});
@ -55,7 +55,7 @@ describe('FieldValueCell', () => {
beforeEach(() => {
render(
<TestProviders>
<FieldValueCell
<TableFieldValueCell
contextId={contextId}
data={hostIpData}
eventId={eventId}
@ -66,17 +66,11 @@ describe('FieldValueCell', () => {
);
});
test('it renders each of the expected values when `fieldFromBrowserField` is undefined', () => {
it('should render each of the expected values when `fieldFromBrowserField` is undefined', () => {
hostIpValues.forEach((value) => {
expect(screen.getByText(value)).toBeInTheDocument();
});
});
test('it renders values formatted as plain text (without `eventFieldsTable__fieldValue` formatting)', () => {
expect(screen.getByTestId(`event-field-${hostIpData.field}`).firstChild).not.toHaveClass(
'eventFieldsTable__fieldValue'
);
});
});
describe('`message` field formatting', () => {
@ -105,7 +99,7 @@ describe('FieldValueCell', () => {
beforeEach(() => {
render(
<TestProviders>
<FieldValueCell
<TableFieldValueCell
contextId={contextId}
data={messageData}
eventId={eventId}
@ -116,11 +110,11 @@ describe('FieldValueCell', () => {
);
});
test('it renders special formatting for the `message` field', () => {
it('should render special formatting for the `message` field', () => {
expect(screen.getByTestId('event-field-message')).toBeInTheDocument();
});
test('it renders the expected message value', () => {
it('should render the expected message value', () => {
messageValues.forEach((value) => {
expect(screen.getByText(value)).toBeInTheDocument();
});
@ -139,7 +133,7 @@ describe('FieldValueCell', () => {
beforeEach(() => {
render(
<TestProviders>
<FieldValueCell
<TableFieldValueCell
contextId={contextId}
data={hostIpData}
eventId={eventId}
@ -150,23 +144,17 @@ describe('FieldValueCell', () => {
);
});
test('it renders values formatted with the expected class', () => {
expect(screen.getByTestId(`event-field-${hostIpData.field}`).firstChild).toHaveClass(
'eventFieldsTable__fieldValue'
);
});
test('it aligns items at the start of the group to prevent content from stretching (by default)', () => {
it('should align items at the start of the group to prevent content from stretching (by default)', () => {
expect(screen.getByTestId(`event-field-${hostIpData.field}`).className).toContain(
'flexStart'
);
});
test('it renders link buttons for each of the host ip addresses', () => {
it('should render link buttons for each of the host ip addresses', () => {
expect(screen.getAllByRole('button').length).toBe(hostIpValues.length);
});
test('it renders each of the expected values when `fieldFromBrowserField` is provided', () => {
it('should render each of the expected values when `fieldFromBrowserField` is provided', () => {
hostIpValues.forEach((value) => {
expect(screen.getByText(value)).toBeInTheDocument();
});

View file

@ -0,0 +1,105 @@
/*
* 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, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import type { BrowserField } from '@kbn/timelines-plugin/common';
import { getFieldFormat } from '../../../../common/components/event_details/get_field_format';
import type { EventFieldsData } from '../../../../common/components/event_details/types';
import { OverflowField } from '../../../../common/components/tables/helpers';
import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field';
import { MESSAGE_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
export interface FieldValueCellProps {
/**
* Value used to create a unique identifier in children components
*/
contextId: string;
/**
* Datq retrieved from the row
*/
data: EventFieldsData;
/**
* Id of the document
*/
eventId: string;
/**
* Field retrieved from the BrowserField
*/
fieldFromBrowserField?: Partial<BrowserField>;
/**
* Value of the link field if it exists. Allows to navigate to other pages like host, user, network...
*/
getLinkValue?: (field: string) => string | null;
/**
* Values for the field, to render in the second column of the table
*/
values: string[] | null | undefined;
}
/**
* Renders the value of a field in the second column of the table
*/
export const TableFieldValueCell = memo(
({
contextId,
data,
eventId,
fieldFromBrowserField,
getLinkValue,
values,
}: FieldValueCellProps) => {
if (values == null) {
return null;
}
return (
<EuiFlexGroup
data-test-subj={`event-field-${data.field}`}
direction="column"
gutterSize="none"
>
{values.map((value, i) => {
if (fieldFromBrowserField == null) {
return (
<EuiFlexItem grow={false} key={`${i}-${value}`}>
<EuiText size="xs" key={`${i}-${value}`}>
{value}
</EuiText>
</EuiFlexItem>
);
}
return (
<EuiFlexItem grow={false} key={`${i}-${value}`}>
{data.field === MESSAGE_FIELD_NAME ? (
<OverflowField value={value} />
) : (
<FormattedFieldValue
contextId={`${contextId}-${eventId}-${data.field}-${i}-${value}`}
eventId={eventId}
fieldFormat={getFieldFormat(data)}
fieldName={data.field}
fieldFromBrowserField={fieldFromBrowserField}
fieldType={data.type}
isAggregatable={fieldFromBrowserField.aggregatable}
isDraggable={false}
isObjectArray={data.isObjectArray}
value={value}
linkValue={getLinkValue && getLinkValue(data.field)}
truncate={false}
/>
)}
</EuiFlexItem>
);
})}
</EuiFlexGroup>
);
}
);
TableFieldValueCell.displayName = 'TableFieldValueCell';

View file

@ -8,6 +8,14 @@
import { PREFIX } from '../../../shared/test_ids';
import { CONTENT_TEST_ID, HEADER_TEST_ID } from './expandable_section';
/* Table */
const FLYOUT_TABLE_TEST_ID = `${PREFIX}Table` as const;
export const FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID =
`${FLYOUT_TABLE_TEST_ID}FieldNameCellIcon` as const;
export const FLYOUT_TABLE_FIELD_NAME_CELL_TEXT_TEST_ID =
`${FLYOUT_TABLE_TEST_ID}FieldNameCellText` as const;
/* Header */
const FLYOUT_HEADER_TEST_ID = `${PREFIX}Header` as const;

View file

@ -7,10 +7,13 @@
import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DocumentDetailsContext } from '../../shared/context';
import { TABLE_TAB_CONTENT_TEST_ID } from './test_ids';
import { TABLE_TAB_CONTENT_TEST_ID, TABLE_TAB_SEARCH_INPUT_TEST_ID } from './test_ids';
import { TableTab } from './table_tab';
import { TestProviders } from '../../../../common/mock';
import { mockContextValue } from '../../shared/mocks/mock_context';
import { FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID } from '../components/test_ids';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
@ -40,4 +43,35 @@ describe('<TableTab />', () => {
expect(getByTestId(TABLE_TAB_CONTENT_TEST_ID)).toBeInTheDocument();
});
it('should renders the column headers and a field/value pair', () => {
const { getAllByTestId, getByText } = render(
<TestProviders>
<DocumentDetailsContext.Provider value={mockContextValue}>
<TableTab />
</DocumentDetailsContext.Provider>
</TestProviders>
);
expect(getByText('Field')).toBeInTheDocument();
expect(getByText('Value')).toBeInTheDocument();
expect(getByText('kibana.alert.workflow_status')).toBeInTheDocument();
expect(getByText('open')).toBeInTheDocument();
expect(getAllByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID).length).toBeGreaterThan(0);
});
it('should filter the table correctly', () => {
const { getByTestId, queryByTestId, queryByText } = render(
<TestProviders>
<DocumentDetailsContext.Provider value={mockContextValue}>
<TableTab />
</DocumentDetailsContext.Provider>
</TestProviders>
);
userEvent.type(getByTestId(TABLE_TAB_SEARCH_INPUT_TEST_ID), 'test');
expect(queryByText('kibana.alert.workflow_status')).not.toBeInTheDocument();
expect(queryByText('open')).not.toBeInTheDocument();
expect(queryByTestId(FLYOUT_TABLE_FIELD_NAME_CELL_ICON_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -5,46 +5,104 @@
* 2.0.
*/
import React, { memo } from 'react';
import { EuiText } from '@elastic/eui';
import { getFieldFromBrowserField } from '../../../../common/components/event_details/columns';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { getOr, sortBy } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import { css } from '@emotion/react';
import { type EuiBasicTableColumn, EuiText, EuiInMemoryTable, useEuiFontSize } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { dataTableSelectors, tableDefaults } from '@kbn/securitysolution-data-table';
import { getCategory } from '@kbn/triggers-actions-ui-plugin/public';
import type {
BrowserField,
BrowserFields,
TimelineEventsDetailsItem,
} from '@kbn/timelines-plugin/common';
import { TableFieldNameCell } from '../components/table_field_name_cell';
import { TableFieldValueCell } from '../components/table_field_value_cell';
import { TABLE_TAB_CONTENT_TEST_ID, TABLE_TAB_SEARCH_INPUT_TEST_ID } from './test_ids';
import { getAllFieldsByName } from '../../../../common/containers/source';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { timelineDefaults } from '../../../../timelines/store/defaults';
import { timelineSelectors } from '../../../../timelines/store';
import type { EventFieldsData } from '../../../../common/components/event_details/types';
import { FieldValueCell } from '../../../../common/components/event_details/table/field_value_cell';
import { FieldNameCell } from '../../../../common/components/event_details/table/field_name_cell';
import { CellActions } from '../components/cell_actions';
import * as i18n from '../../../../common/components/event_details/translations';
import { useDocumentDetailsContext } from '../../shared/context';
import type { ColumnsProvider } from '../../../../common/components/event_details/event_fields_browser';
import { EventFieldsBrowser } from '../../../../common/components/event_details/event_fields_browser';
import { TimelineTabs } from '../../../../../common/types';
import { isInTableScope, isTimelineScope } from '../../../../helpers';
export const getColumns: ColumnsProvider = ({
browserFields,
eventId,
contextId,
scopeId,
getLinkValue,
isDraggable,
}) => [
const COUNT_PER_PAGE_OPTIONS = [25, 50, 100];
const PLACEHOLDER = i18n.translate('xpack.securitySolution.flyout.table.filterPlaceholderLabel', {
defaultMessage: 'Filter by field or value...',
});
export const FIELD = i18n.translate('xpack.securitySolution.flyout.table.fieldCellLabel', {
defaultMessage: 'Field',
});
const VALUE = i18n.translate('xpack.securitySolution.flyout.table.valueCellLabel', {
defaultMessage: 'Value',
});
/**
* Defines the behavior of the search input that appears above the table of data
*/
const search = {
box: {
incremental: true,
placeholder: PLACEHOLDER,
schema: true,
'data-test-subj': TABLE_TAB_SEARCH_INPUT_TEST_ID,
},
};
/**
* Retrieve the correct field from the BrowserField
*/
export const getFieldFromBrowserField = memoizeOne(
(field: string, browserFields: BrowserFields): BrowserField | undefined => {
const category = getCategory(field);
return browserFields[category]?.fields?.[field] as BrowserField;
},
(newArgs, lastArgs) => newArgs[0] === lastArgs[0]
);
export type ColumnsProvider = (providerOptions: {
/**
* An object containing fields by type
*/
browserFields: BrowserFields;
/**
* Id of the document
*/
eventId: string;
/**
* Maintain backwards compatibility // TODO remove when possible
*/
scopeId: string;
/**
* Value of the link field if it exists. Allows to navigate to other pages like host, user, network...
*/
getLinkValue: (field: string) => string | null;
}) => Array<EuiBasicTableColumn<TimelineEventsDetailsItem>>;
export const getColumns: ColumnsProvider = ({ browserFields, eventId, scopeId, getLinkValue }) => [
{
field: 'field',
name: (
<EuiText size="xs">
<strong>{i18n.FIELD}</strong>
<strong>{FIELD}</strong>
</EuiText>
),
width: '30%',
render: (field, data) => {
return (
<FieldNameCell data={data as EventFieldsData} field={field} fieldMapping={undefined} />
);
return <TableFieldNameCell dataType={(data as EventFieldsData).type} field={field} />;
},
},
{
field: 'values',
name: (
<EuiText size="xs">
<strong>{i18n.VALUE}</strong>
<strong>{VALUE}</strong>
</EuiText>
),
width: '70%',
@ -52,13 +110,12 @@ export const getColumns: ColumnsProvider = ({
const fieldFromBrowserField = getFieldFromBrowserField(data.field, browserFields);
return (
<CellActions field={data.field} value={values} isObjectArray={data.isObjectArray}>
<FieldValueCell
contextId={contextId}
<TableFieldValueCell
contextId={scopeId}
data={data as EventFieldsData}
eventId={eventId}
fieldFromBrowserField={fieldFromBrowserField}
getLinkValue={getLinkValue}
isDraggable={isDraggable}
values={values}
/>
</CellActions>
@ -68,23 +125,106 @@ export const getColumns: ColumnsProvider = ({
];
/**
* Table view displayed in the document details expandable flyout right section
* Table view displayed in the document details expandable flyout right section Table tab
*/
// TODO: MOVE TO FLYOUT FOLDER - https://github.com/elastic/security-team/issues/7462
export const TableTab = memo(() => {
const smallFontSize = useEuiFontSize('xs').fontSize;
const { browserFields, dataFormattedForFieldBrowser, eventId, scopeId } =
useDocumentDetailsContext();
const [pagination, setPagination] = useState<{ pageIndex: number }>({
pageIndex: 0,
});
const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => {
setPagination({ pageIndex: index });
}, []);
const getScope = useMemo(() => {
if (isTimelineScope(scopeId)) {
return timelineSelectors.getTimelineByIdSelector();
} else if (isInTableScope(scopeId)) {
return dataTableSelectors.getTableByIdSelector();
}
}, [scopeId]);
const defaults = useMemo(
() => (isTimelineScope(scopeId) ? timelineDefaults : tableDefaults),
[scopeId]
);
const columnHeaders = useDeepEqualSelector((state) => {
const { columns } = (getScope && getScope(state, scopeId)) ?? defaults;
return columns;
});
const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
const items = useMemo(
() =>
sortBy(['field'], dataFormattedForFieldBrowser).map((item, i) => ({
...item,
...fieldsByName[item.field],
valuesConcatenated: item.values != null ? item.values.join() : '',
ariaRowindex: i + 1,
})),
[dataFormattedForFieldBrowser, fieldsByName]
);
const getLinkValue = useCallback(
(field: string) => {
const columnHeader = columnHeaders.find((col) => col.id === field);
if (!columnHeader || !columnHeader.linkField) {
return null;
}
const linkFieldData = (dataFormattedForFieldBrowser ?? []).find(
(d) => d.field === columnHeader.linkField
);
const linkFieldValue = getOr(null, 'originalValue', linkFieldData);
return Array.isArray(linkFieldValue) ? linkFieldValue[0] : linkFieldValue;
},
[dataFormattedForFieldBrowser, columnHeaders]
);
// forces the rows of the table to render smaller fonts
const onSetRowProps = useCallback(
({ field }: TimelineEventsDetailsItem) => ({
className: 'flyout-table-row-small-font',
'data-test-subj': `flyout-table-row-${field}`,
}),
[]
);
const columns = useMemo(
() =>
getColumns({
browserFields,
eventId,
scopeId,
getLinkValue,
}),
[browserFields, eventId, scopeId, getLinkValue]
);
return (
<EventFieldsBrowser
browserFields={browserFields}
data={dataFormattedForFieldBrowser}
eventId={eventId}
isDraggable={false}
timelineTabType={TimelineTabs.query}
scopeId={scopeId}
isReadOnly={false}
columnsProvider={getColumns}
<EuiInMemoryTable
items={items}
itemId="field"
columns={columns}
onTableChange={onTableChange}
pagination={{
...pagination,
pageSizeOptions: COUNT_PER_PAGE_OPTIONS,
}}
rowProps={onSetRowProps}
search={search}
sorting={false}
data-test-subj={TABLE_TAB_CONTENT_TEST_ID}
css={css`
.euiTableRow {
font-size: ${smallFontSize};
}
`}
/>
);
});

View file

@ -7,6 +7,7 @@
import { PREFIX } from '../../../shared/test_ids';
export const TABLE_TAB_CONTENT_TEST_ID = 'event-fields-browser' as const;
export const TABLE_TAB_CONTENT_TEST_ID = `${PREFIX}DocumentTable` as const;
export const TABLE_TAB_SEARCH_INPUT_TEST_ID = `${PREFIX}DocumentTableSearchInput` as const;
export const JSON_TAB_CONTENT_TEST_ID = 'jsonView' as const;
export const JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID = `${PREFIX}JsonTabCopyToClipboard` as const;

View file

@ -35942,8 +35942,6 @@
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "Enrichi avec la Threat Intelligence",
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "Affiche des renseignements supplémentaires sur les menaces (Threat Intelligence) concernant l'alerte. La recherche a porté sur les 30 derniers jours par défaut.",
"xpack.securitySolution.eventDetails.description": "Description",
"xpack.securitySolution.eventDetails.field": "Champ",
"xpack.securitySolution.eventDetails.filter.placeholder": "Filtre par Champ, Valeur ou Description...",
"xpack.securitySolution.eventDetails.multiFieldBadge": "champ multiple",
"xpack.securitySolution.eventDetails.multiFieldTooltipContent": "Les champs multiples peuvent avoir plusieurs valeurs.",
"xpack.securitySolution.eventDetails.osqueryView": "Résultats Osquery",
@ -35955,7 +35953,6 @@
"xpack.securitySolution.eventDetails.summaryView": "résumé",
"xpack.securitySolution.eventDetails.table": "Tableau",
"xpack.securitySolution.eventDetails.table.actions": "Actions",
"xpack.securitySolution.eventDetails.value": "Valeur",
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\" a été ajouté à la liste de filtres d'événements.",
"xpack.securitySolution.eventFilter.form.description.placeholder": "Description",
"xpack.securitySolution.eventFilter.form.name.error": "Le nom doit être indiqué",
@ -36200,18 +36197,6 @@
"xpack.securitySolution.fieldBrowser.removeButtonDescription": "Supprimer un champ de temps d'exécution",
"xpack.securitySolution.fieldBrowser.runtimeLabel": "Temps d'exécution",
"xpack.securitySolution.fieldBrowser.runtimeTitle": "Champ de temps d'exécution",
"xpack.securitySolution.fieldNameIcons.booleanAriaLabel": "Champ booléen",
"xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel": "Champ conflictuel",
"xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel": "Champ de date",
"xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel": "Champ de point géographique",
"xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel": "Champ de forme géométrique",
"xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel": "Champ d'adresse IP",
"xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel": "Champ Murmur3",
"xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel": "Champ imbriqué",
"xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel": "Champ numérique",
"xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel": "Champ source",
"xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel": "Champ de chaîne",
"xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel": "Champ inconnu",
"xpack.securitySolution.fieldRenderers.moreLabel": "Plus",
"xpack.securitySolution.filtersGroup.assignees.buttonTitle": "Utilisateurs affectés",
"xpack.securitySolution.filtersGroup.assignees.popoverTooltip": "Filtrer par utilisateurs assignés",

View file

@ -35812,8 +35812,6 @@
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "脅威インテリジェンスで強化",
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "アラートに関する追加の脅威インテリジェンスを表示します。デフォルトでは過去30日分が照会されます。",
"xpack.securitySolution.eventDetails.description": "説明",
"xpack.securitySolution.eventDetails.field": "フィールド",
"xpack.securitySolution.eventDetails.filter.placeholder": "フィールド、値、または説明でフィルター...",
"xpack.securitySolution.eventDetails.multiFieldBadge": "複数フィールド",
"xpack.securitySolution.eventDetails.multiFieldTooltipContent": "複数フィールドにはフィールドごとに複数の値を入力できます",
"xpack.securitySolution.eventDetails.osqueryView": "Osquery結果",
@ -35825,7 +35823,6 @@
"xpack.securitySolution.eventDetails.summaryView": "まとめ",
"xpack.securitySolution.eventDetails.table": "表",
"xpack.securitySolution.eventDetails.table.actions": "アクション",
"xpack.securitySolution.eventDetails.value": "値",
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "\"{name}\"がイベントフィルターリストに追加されました。",
"xpack.securitySolution.eventFilter.form.description.placeholder": "説明",
"xpack.securitySolution.eventFilter.form.name.error": "名前を空にすることはできません",
@ -36070,18 +36067,6 @@
"xpack.securitySolution.fieldBrowser.removeButtonDescription": "ランタイムフィールドを削除",
"xpack.securitySolution.fieldBrowser.runtimeLabel": "ランタイム",
"xpack.securitySolution.fieldBrowser.runtimeTitle": "ランタイムフィールド",
"xpack.securitySolution.fieldNameIcons.booleanAriaLabel": "ブールフィールド",
"xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel": "矛盾フィールド",
"xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel": "日付フィールド",
"xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイントフィールド",
"xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報シェイプフィールド",
"xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel": "IPアドレスフィールド",
"xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3フィールド",
"xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel": "入れ子フィールド",
"xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel": "数値フィールド",
"xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド",
"xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel": "文字列フィールド",
"xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド",
"xpack.securitySolution.fieldRenderers.moreLabel": "詳細",
"xpack.securitySolution.filtersGroup.assignees.buttonTitle": "担当者",
"xpack.securitySolution.filtersGroup.assignees.popoverTooltip": "担当者でフィルター",

View file

@ -35982,8 +35982,6 @@
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "已使用威胁情报扩充",
"xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "显示该告警的其他威胁情报。默认会查询过去 30 天的数据。",
"xpack.securitySolution.eventDetails.description": "描述",
"xpack.securitySolution.eventDetails.field": "字段",
"xpack.securitySolution.eventDetails.filter.placeholder": "按字段、值或描述筛选......",
"xpack.securitySolution.eventDetails.multiFieldBadge": "多字段",
"xpack.securitySolution.eventDetails.multiFieldTooltipContent": "多字段的每个字段可以有多个值",
"xpack.securitySolution.eventDetails.osqueryView": "Osquery 结果",
@ -35995,7 +35993,6 @@
"xpack.securitySolution.eventDetails.summaryView": "摘要",
"xpack.securitySolution.eventDetails.table": "表",
"xpack.securitySolution.eventDetails.table.actions": "操作",
"xpack.securitySolution.eventDetails.value": "值",
"xpack.securitySolution.eventFilter.flyoutForm.creationSuccessToastTitle": "“{name}”已添加到事件筛选列表。",
"xpack.securitySolution.eventFilter.form.description.placeholder": "描述",
"xpack.securitySolution.eventFilter.form.name.error": "名称不能为空",
@ -36240,18 +36237,6 @@
"xpack.securitySolution.fieldBrowser.removeButtonDescription": "删除运行时字段",
"xpack.securitySolution.fieldBrowser.runtimeLabel": "运行时",
"xpack.securitySolution.fieldBrowser.runtimeTitle": "运行时字段",
"xpack.securitySolution.fieldNameIcons.booleanAriaLabel": "布尔值字段",
"xpack.securitySolution.fieldNameIcons.conflictFieldAriaLabel": "冲突字段",
"xpack.securitySolution.fieldNameIcons.dateFieldAriaLabel": "日期字段",
"xpack.securitySolution.fieldNameIcons.geoPointFieldAriaLabel": "地理点字段",
"xpack.securitySolution.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段",
"xpack.securitySolution.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址字段",
"xpack.securitySolution.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 字段",
"xpack.securitySolution.fieldNameIcons.nestedFieldAriaLabel": "嵌套字段",
"xpack.securitySolution.fieldNameIcons.numberFieldAriaLabel": "数字字段",
"xpack.securitySolution.fieldNameIcons.sourceFieldAriaLabel": "源字段",
"xpack.securitySolution.fieldNameIcons.stringFieldAriaLabel": "字符串字段",
"xpack.securitySolution.fieldNameIcons.unknownFieldAriaLabel": "未知字段",
"xpack.securitySolution.fieldRenderers.moreLabel": "更多",
"xpack.securitySolution.filtersGroup.assignees.buttonTitle": "被分配人",
"xpack.securitySolution.filtersGroup.assignees.popoverTooltip": "按被分配人筛选",

View file

@ -5,22 +5,23 @@
* 2.0.
*/
import { getClassSelector, getDataTestSubjectSelector } from '../../helpers/common';
import { getDataTestSubjectSelector } from '../../helpers/common';
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_FILTER = getClassSelector('euiFieldSearch');
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_FILTER = getDataTestSubjectSelector(
'securitySolutionFlyoutDocumentTableSearchInput'
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW = getDataTestSubjectSelector(
'event-fields-table-row-@timestamp'
'flyout-table-row-@timestamp'
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_CELL =
getDataTestSubjectSelector('event-field-@timestamp');
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW = getDataTestSubjectSelector(
'event-fields-table-row-_id'
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW =
getDataTestSubjectSelector('flyout-table-row-_id');
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_HOST_OS_BUILD_ROW = getDataTestSubjectSelector(
'event-fields-table-row-host.os.build'
'flyout-table-row-host.os.build'
);
export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_THREAT_ENRICHMENTS = getDataTestSubjectSelector(
'event-fields-table-row-threat.enrichments'
'flyout-table-row-threat.enrichments'
);
const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ACTIONS =
'actionItem-security-detailsFlyout-cellActions-';